Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 100 additions & 10 deletions packages/opencode/src/altimate/workspace/awareness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,25 @@
// still be in the catalog while routing refuses them, and silence would leave the
// model free to call what it can see. `DISABLED_COPY` below is the decision table.
//
// Extension-type tools (served through a live VS Code bridge) are the section's other
// list. They shadow nothing, so they are awareness only; they are named only when
// precedence has them as really served (in the catalog AND behind a live bridge —
// see `extensionsServed` in precedence.ts), and a dormant bridge is silence, not a
// warning. One consequence for the table: `nothing-materialised` speaks when it
// carries extension tools — a workspace can serve those and no warehouse capability
// at all — and stays silent otherwise, which keeps the byte-identical claim intact.
//
// SERVER-SIDE ONLY, for the same reason `precedence.ts` is: the TUI plugin runtime
// loads plugins in a separate module realm, so an import from there would read a
// different, always-empty `Precedence` map. Import this only from the session layer.
import { type Capability, type Precedence, inertWorkspaceName, servedInventory } from "./precedence"
import {
type Capability,
type Precedence,
type ServedExtension,
inertWorkspaceName,
servedExtensions,
servedInventory,
} from "./precedence"

/** Hard ceiling on the rendered section. Deliberately independent of
* `UNIFIED_INJECTION_BUDGET`: this is a routing directive, not knowledge, and must
Expand Down Expand Up @@ -130,10 +145,25 @@ const DISABLED_COPY: Record<NonNullable<Precedence["disabledReason"]>, string> =
*/
export function systemSection(precedence: Precedence | undefined): string {
if (!precedence) return ""
if (!precedence.enabled) return precedence.disabledReason ? DISABLED_COPY[precedence.disabledReason] : ""
const extLines = servedExtensions(precedence).map(extensionLine)
if (!precedence.enabled) {
// The one disabled state that can carry served extension tools (see `derive`):
// no warehouse capability is routed, but the bridge is serving, and silence
// would leave the model unaware of tools it can see. Without them the table's
// entry renders exactly as before.
if (precedence.disabledReason === "nothing-materialised" && extLines.length > 0) {
return assembleExtensionsOnly(precedence.workspaceName, precedence.workspaceId, extLines)
}
return precedence.disabledReason ? DISABLED_COPY[precedence.disabledReason] : ""
}

const served = servedInventory(precedence)
if (served.length === 0) return ""
// Enabled but no warehouse capability reachable (the analyst shape). The same
// ruleset filters the extension tools, so normally none survive either; any that
// do are still real and still callable, so they are said.
if (served.length === 0) {
return extLines.length > 0 ? assembleExtensionsOnly(precedence.workspaceName, precedence.workspaceId, extLines) : ""
}

// `type` is the canonical local driver type (`postgres`), not the user-facing
// connection name nor the engine's integration id (`postgresql`) — it is what the
Expand All @@ -147,7 +177,53 @@ export function systemSection(precedence: Precedence | undefined): string {
return `- ${type} — ${servedPart}${localPart}`
})

return assemble(precedence.workspaceName, precedence.workspaceId, typeLines)
return assemble(precedence.workspaceName, precedence.workspaceId, typeLines, extLines)
}

/** One extension-type integration and every tool of it the caller can call. The
* integration name is catalog-authored and precedence already made it inert; the
* keys are engine tool names, quoted the way the warehouse lines quote theirs. */
function extensionLine(group: ServedExtension): string {
return `- ${group.integration} — ${group.tools.map((t) => `\`${t.modelKey}\``).join(", ")}`
}

/** Above the extension lines in both shapes of the section. It names the condition
* the tools depend on, so a failure after the window closes can be explained
* rather than retried blindly. */
const EXTENSION_INTRO =
"The VS Code window open on this project serves these extension tools through the workspace. Call them " +
"like any other tool; they are unavailable while that window is closed:"

const extensionOmission = (n: number) =>
`- …and ${n} further extension integration${n === 1 ? "" : "s"} served through the connected VS Code window.`

/** The section when extension tools are served and no warehouse capability is
* routed. The local-tools sentence is kept: with nothing shadowed, every
* connection really does stay local, and the model should not infer otherwise
* from seeing `datamate_*` keys listed. Same cap, same drop rule as `assemble`. */
function assembleExtensionsOnly(workspaceName: string, workspaceId: string | undefined, extLines: string[]): string {
const label = workspaceLabel(workspaceName, workspaceId)
const render = (ext: string[]) => {
const omitted = extLines.length - ext.length
return [
HEADING,
"",
`This project is bound to Altimate workspace ${label}. No warehouse capability is routed through it in ` +
`this session: every connection uses the local tools (${ALL_LOCAL_TOOLS}).`,
"",
EXTENSION_INTRO,
"",
...ext,
...(omitted > 0 ? [extensionOmission(omitted)] : []),
].join("\n")
}
let ext = extLines
let out = render(ext)
while (out.length > MAX_SECTION_CHARS && ext.length > 0) {
ext = ext.slice(0, -1)
out = render(ext)
}
return out
}

/** The workspace name is customer-authored and lands in the system prompt — the
Expand All @@ -171,10 +247,16 @@ function workspaceLabel(name: string, id: string | undefined): string {
* be partial instead, and the prohibition is kept only for types the workspace does
* not serve. The count is stated once, on the list where it belongs; the converse
* carries only what the model should DO about the omission. */
function assemble(workspaceName: string, workspaceId: string | undefined, typeLines: string[]): string {
function assemble(
workspaceName: string,
workspaceId: string | undefined,
typeLines: string[],
extLines: string[] = [],
): string {
const label = workspaceLabel(workspaceName, workspaceId)
const render = (lines: string[]) => {
const render = (lines: string[], ext: string[]) => {
const omitted = typeLines.length - lines.length
const extOmitted = extLines.length - ext.length
const converse =
omitted > 0
? "For the served types omitted above, prefer the `datamate_*` tool for that type when one is in the " +
Expand All @@ -193,16 +275,24 @@ function assemble(workspaceName: string, workspaceId: string | undefined, typeLi
...(omitted > 0
? [`- …and ${omitted} further connection type${omitted === 1 ? "" : "s"} served by this workspace.`]
: []),
...(extLines.length > 0
? ["", EXTENSION_INTRO, "", ...ext, ...(extOmitted > 0 ? [extensionOmission(extOmitted)] : [])]
: []),
"",
converse,
].join("\n")
}

let lines = typeLines
let out = render(lines)
while (out.length > MAX_SECTION_CHARS && lines.length > 0) {
lines = lines.slice(0, -1)
out = render(lines)
let ext = extLines
let out = render(lines, ext)
// Extension lines are dropped first: they are awareness, while the type lines
// are directives the guard will enforce, and a redirect the model was never
// warned of is the worse failure. Type lines go only once none are left.
while (out.length > MAX_SECTION_CHARS && (ext.length > 0 || lines.length > 0)) {
if (ext.length > 0) ext = ext.slice(0, -1)
else lines = lines.slice(0, -1)
out = render(lines, ext)
}
return out
}
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/altimate/workspace/engine-overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ async function reconcile(sessionID: string, directory: string, state: DirectoryS
kind: "attached",
available: present.size,
...(declared ? { declared: declared.keys.length, missing } : {}),
...(declared?.extensions?.length ? { extensions: declared.extensions } : {}),
}
const rec = record(sessionID, outcome)
// Keyed on the workspace too: a re-link with an identical inventory is still
Expand Down
19 changes: 14 additions & 5 deletions packages/opencode/src/altimate/workspace/engine-probes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { EventV2Bridge } from "@/event-v2-bridge"
import { TuiEvent } from "@/server/tui-event"
import { readLocalBindingScopedStrict } from "./state"
import { log, syncInternals, type BindingRead, type ScopedBinding } from "./engine-seams"
import type { Declared, Toast } from "./engine-types"
import type { Declared, DeclaredExtension, Toast } from "./engine-types"

/** How long the allowlist lookup may hold a turn. Once per workspace per process. */
export const DECLARED_TIMEOUT_MS = 4_000
Expand Down Expand Up @@ -129,14 +129,23 @@ export async function declared(workspaceId: string): Promise<Declared | null> {
AltimateApi.getDatamate(workspaceId),
AltimateApi.listIntegrations(),
])
const extensionIds = new Set(catalog.filter((i) => i.type === "extension").map((i) => i.id))
const extensionNames = new Map(
catalog.filter((i) => i.type === "extension").map((i): [string, string] => [i.id, i.name ?? i.id]),
)
const keys: string[] = []
const extensionKeys: string[] = []
const extensions: DeclaredExtension[] = []
for (const integration of workspace.integrations ?? []) {
const target = extensionIds.has(integration.id) ? extensionKeys : keys
for (const tool of integration.tools ?? []) target.push(tool.key)
const toolKeys = (integration.tools ?? []).map((tool) => tool.key)
const name = extensionNames.get(integration.id)
if (name === undefined) {
keys.push(...toolKeys)
continue
}
extensionKeys.push(...toolKeys)
if (toolKeys.length > 0) extensions.push({ id: integration.id, name, keys: toolKeys })
}
return { keys, extensionKeys }
return { keys, extensionKeys, ...(extensions.length > 0 ? { extensions } : {}) }
} catch (err) {
log.warn("could not read the declared workspace integrations", { workspaceId, err: String(err) })
return null
Expand Down
22 changes: 20 additions & 2 deletions packages/opencode/src/altimate/workspace/engine-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,16 @@ export const TOOL_PREFIX = `${DATAMATE_KEY}_`
export type Outcome =
| { kind: "disabled" }
| { kind: "unbound" }
| { kind: "attached"; available: number; declared?: number; missing?: string[] }
| {
kind: "attached"
available: number
declared?: number
missing?: string[]
/** The allowlist's extension-type integrations, when it names any: what a
* live IDE bridge could serve. Whether they are present is decided per turn
* against the catalog, never recorded here. */
extensions?: DeclaredExtension[]
}
| { kind: "engine-missing"; declared?: number }
/** `found` is null when the binary ran but printed nothing usable — broken
* rather than old; the message says so. */
Expand Down Expand Up @@ -57,7 +66,16 @@ export type McpStatus = Record<string, { status: string; error?: string } | unde
/** Declared allowlist for a workspace, split by whether the CLI can serve it.
* Extension-type integrations are RPC into a live VS Code host and have no
* meaning on the CLI surface, so they are excluded from the reported gap. */
export type Declared = { keys: string[]; extensionKeys: string[] }
export type Declared = {
keys: string[]
extensionKeys: string[]
/** The extension keys again, grouped under their catalog integration, for the
* surfaces that name them rather than count them. Optional: the flat lists are
* the contract every existing reader was written against. */
extensions?: DeclaredExtension[]
}

export type DeclaredExtension = { id: string; name: string; keys: string[] }

/** A configured MCP entry in either shape it can reach us: opencode's own
* `command: string[]` argv, or the `{ command, args }` split an IDE writes. */
Expand Down
108 changes: 99 additions & 9 deletions packages/opencode/src/altimate/workspace/precedence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ import {
type Outcome,
} from "./engine-overlay"
import { readLocalBindingScopedStrict } from "./state"
import { liveBridge } from "./engine-probes"
import { syncInternals } from "./engine-seams"
import { canonicalType } from "../native/connections/registry"
import * as Registry from "../native/connections/registry"

Expand Down Expand Up @@ -148,6 +150,19 @@ export interface Precedence {
* engine keys, so redirecting its permitted reads would take away the one thing
* it exists to do. Absent means "unknown", which is treated as reachable. */
ruleset?: PermissionNext.Ruleset
/** Extension-type tools the engine is serving through a live IDE bridge, grouped
* by integration. Nothing here is shadowed — there is no native counterpart to
* redirect — so this is awareness only, and it is present on a
* `nothing-materialised` snapshot too: a workspace can serve extension tools and
* no warehouse capability at all. Absent or empty when no bridge is live. */
extensions?: ServedExtension[]
}

/** One extension-type integration and the tools of it that materialised. */
export interface ServedExtension {
/** The catalog's display name, made inert the same way the workspace name is. */
integration: string
tools: { engineTool: string; modelKey: string }[]
}

/** The workspace name as model-visible text: control characters stripped (C0, DEL and
Expand Down Expand Up @@ -242,14 +257,52 @@ function remember(sessionID: string, value: Precedence): void {
* a notice. Being wrong in that direction costs a turn's routing, which the next turn
* repairs; being wrong the other way routes credentials into someone else's engine.
*/
async function attested(sessionID: string): Promise<boolean> {
const outcome = precedenceInternals.attachOutcome
async function attachOutcome(sessionID: string): Promise<Outcome | undefined> {
return precedenceInternals.attachOutcome
? await precedenceInternals.attachOutcome().catch(() => undefined)
: settledOutcome(sessionID)
if (!outcome) return false
// The attach module owns the allowlist; a new outcome kind refuses until it is
// named there (see SERVING in engine-types).
return attributableEngine(outcome)
}

/** The project directory this process serves, or null outside an instance. Read
* defensively for the same reason `currentBinding` is: the accessor throws when
* there is no instance, and a bridge probe must never cost the turn its tools. */
function projectDirectory(): string | null {
try {
return Instance.directory || null
} catch {
return null
}
}

/**
* Extension-type tools the session really has, grouped by integration. Two signals
* must agree, in the same spirit as attribution: the key is in the live catalog
* (the engine held a bridge when it spawned and is serving the tool now) AND a
* bridge for this project is live at this turn (the window it needs is still
* open). The catalog alone would go on advertising tools whose window has since
* closed — the engine discovers the bridge at spawn and does not re-list; the
* bridge alone says nothing about what materialised. Either missing renders
* nothing, which is the silence the awareness section wants for a dormant bridge.
*/
function extensionsServed(outcome: Outcome, present: Set<string>): ServedExtension[] {
if (outcome.kind !== "attached" || !outcome.extensions?.length) return []
const groups: ServedExtension[] = []
for (const ext of outcome.extensions) {
const tools = ext.keys
.filter((key) => present.has(key))
.map((engineTool) => ({ engineTool, modelKey: `${DATAMATE_KEY}_${engineTool}` }))
if (tools.length > 0) groups.push({ integration: inertWorkspaceName(ext.name), tools })
}
if (groups.length === 0) return []
const cwd = projectDirectory()
// No directory and no seam: nothing to match a sidecar against, so no claim.
if (cwd === null && !syncInternals.liveBridge) return []
try {
if (!liveBridge(cwd ?? "")) return []
} catch {
return []
}
return groups
}

/** Sessions whose inventory line has already been reported. Precedence is re-derived
Expand Down Expand Up @@ -534,7 +587,10 @@ async function derive(sessionID: string, tools: Record<string, unknown>): Promis
// one we established; the configured pin says it still names this workspace. Config
// alone is not enough — it can be rewritten under a live connection — and the
// outcome alone would not notice a later rewrite pointing somewhere else.
if (!(await attested(sessionID))) {
// The attach module owns the allowlist; a new outcome kind refuses until it is
// named there (see SERVING in engine-types).
const outcome = await attachOutcome(sessionID)
if (!outcome || !attributableEngine(outcome)) {
log.info("no attach established this session's engine; precedence off", { bound: binding.datamateId })
return EMPTY("unattributed", workspaceName)
}
Expand All @@ -552,6 +608,7 @@ async function derive(sessionID: string, tools: Record<string, unknown>): Promis
warnForeign(sessionID, tools)
if (present.size === 0) return EMPTY("nothing-materialised", workspaceName)
warnUnrecognised(sessionID, present)
const extensions = extensionsServed(outcome, present)

// Mechanism 2 — capability by capability, only where the key is really there.
const shadowed = new Map<string, Map<Capability, ShadowEntry>>()
Expand All @@ -571,8 +628,24 @@ async function derive(sessionID: string, tools: Record<string, unknown>): Promis
})
}
}
if (shadowed.size === 0) return EMPTY("nothing-materialised", workspaceName)
return { workspaceName, workspaceId: String(binding.datamateId), enabled: true, shadowed }
// Extension tools ride on the disabled snapshot too: they are served without any
// warehouse capability being routed, and the model should hear about them either way.
if (shadowed.size === 0) {
// With extension tools aboard the snapshot also names the bound id, as the
// enabled shape does: the section labels the workspace by it. Without them the
// shape is exactly `EMPTY`'s, as it always was.
return {
...EMPTY("nothing-materialised", workspaceName),
...(extensions.length ? { workspaceId: String(binding.datamateId), extensions } : {}),
}
}
return {
workspaceName,
workspaceId: String(binding.datamateId),
enabled: true,
shadowed,
...(extensions.length ? { extensions } : {}),
}
}

/** Read the session's precedence without recomputing it. */
Expand Down Expand Up @@ -684,6 +757,23 @@ export function servedInventory(precedence: Precedence): ServedType[] {
}
return out
}

/**
* The extension-type tools this caller can really call, grouped by integration —
* the awareness section's other list. Same reachability filter as `servedInventory`,
* applied at projection time because the ruleset is attached after derivation; a
* group none of whose tools the caller may call is dropped rather than advertised.
* Deliberately NOT gated on `enabled`: a `nothing-materialised` snapshot carries
* these too (see `derive`).
*/
export function servedExtensions(precedence: Precedence): ServedExtension[] {
const out: ServedExtension[] = []
for (const group of precedence.extensions ?? []) {
const tools = group.tools.filter((t) => reachable(precedence, t.modelKey))
if (tools.length > 0) out.push({ integration: group.integration, tools })
}
return out
}
// altimate_change end

function unreachable(workspaceName: string, modelKey: string): Verdict {
Expand Down
Loading
Loading