Skip to content
Merged
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
24 changes: 18 additions & 6 deletions packages/opencode/src/altimate/workspace/skill-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,11 +772,22 @@ async function hasManagedSnapshot(directory: string): Promise<boolean> {
}
}

/** Whether local state says this project has a workspace: a snapshot a sync
* once published, or a cached binding. Decides if a failed binding lookup is
* worth telling the user about. */
/** Whether this project is known to have a workspace. Decides if a failed
* binding lookup is worth telling the user about. */
async function hasBindingEvidence(directory: string): Promise<boolean> {
if (await hasManagedSnapshot(directory)) return true
// The IDE's pin is a workspace the user chose for this tree, and it is never
// written to disk — an inaccessible pinned workspace would otherwise be
// silent. A malformed pin counts as well: the extension set one, and
// resolution fails closed on it for every directory. Scoped with the same
// `resolveWithinRoot` as the pin purge in `syncSkills`, so the purge and the
// warning always agree on which folders a pin speaks for — and the warning
// still fires after that purge has removed the manifest.
const pin = readPin()
if (pin.kind === "invalid") return true
if (pin.kind === "valid" && resolveWithinRoot(directory, pin.root) !== null) return true
// This client's manifest, not merely a `_workspace` folder: a tree the user
// or another tool created says nothing about a binding.
if ((await readManifest(directory).catch(() => null)) !== null) return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This counts any shallowly parseable .manifest.json as this client’s evidence, so a user or another tool can create a lookalike manifest and make an unlinked project emit offline warnings. Tighten manifest validation or ownership before using it for evidence.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/altimate/workspace/skill-sync.ts, line 790:

<comment>This counts any shallowly parseable `.manifest.json` as this client’s evidence, so a user or another tool can create a lookalike manifest and make an unlinked project emit offline warnings. Tighten manifest validation or ownership before using it for evidence.</comment>

<file context>
@@ -772,11 +772,22 @@ async function hasManagedSnapshot(directory: string): Promise<boolean> {
+  if (pin.kind === "valid" && resolveWithinRoot(directory, pin.root) !== null) return true
+  // This client's manifest, not merely a `_workspace` folder: a tree the user
+  // or another tool created says nothing about a binding.
+  if ((await readManifest(directory).catch(() => null)) !== null) return true
   return (await readLocalBinding(directory).catch(() => null)) !== null
 }
</file context>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new readManifest arm replaces the old hasManagedSnapshot lstat check, so a snapshot this client published whose manifest no longer validates (corrupt write, partial truncation) stops counting as evidence. Its skills still load from disk (discovery ignores the manifest) and deactivate refuses to remove an unreadable manifest, so the workspace keeps serving skills while the binding-lookup warning goes quiet. If narrowing to a fully valid manifest is deliberate, this tradeoff is worth a note; otherwise, damaged snapshots now lose their warning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/altimate/workspace/skill-sync.ts, line 790:

<comment>The new `readManifest` arm replaces the old `hasManagedSnapshot` lstat check, so a snapshot this client published whose manifest no longer validates (corrupt write, partial truncation) stops counting as evidence. Its skills still load from disk (discovery ignores the manifest) and `deactivate` refuses to remove an unreadable manifest, so the workspace keeps serving skills while the binding-lookup warning goes quiet. If narrowing to a fully valid manifest is deliberate, this tradeoff is worth a note; otherwise, damaged snapshots now lose their warning.</comment>

<file context>
@@ -772,11 +772,22 @@ async function hasManagedSnapshot(directory: string): Promise<boolean> {
+  if (pin.kind === "valid" && resolveWithinRoot(directory, pin.root) !== null) return true
+  // This client's manifest, not merely a `_workspace` folder: a tree the user
+  // or another tool created says nothing about a binding.
+  if ((await readManifest(directory).catch(() => null)) !== null) return true
   return (await readLocalBinding(directory).catch(() => null)) !== null
 }
</file context>

return (await readLocalBinding(directory).catch(() => null)) !== null
}

Expand Down Expand Up @@ -930,8 +941,9 @@ export async function syncSkills(directory: string): Promise<SyncResult> {
// workspace. With a local binding row, offline resolves to a stale "bound"
// and fails later at the list; without one, the server is always asked,
// so offline lands here for EVERY opted-in project, including ones never
// linked. A snapshot on disk (a server-side binding that synced before)
// or a local row is that evidence; without either, stay quiet.
// linked. An IDE pin for this tree, this client's manifest (a server-side
// binding that synced before) or a local row is that evidence — see
// `hasBindingEvidence`; without any of them, stay quiet.
if (outcome.status === "unknown" && (await hasBindingEvidence(canon)))
syncError = "could not confirm this project's workspace (offline, or no access to it)"
return
Expand Down
81 changes: 81 additions & 0 deletions packages/opencode/test/altimate/workspace/skill-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,87 @@ describe("workspace skill sync", () => {
expect(result.skipped).toEqual([])
})

// What counts as "this project has a workspace" when the lookup fails.
const offline = () => {
globalThis.fetch = (async () => {
throw new Error("offline")
}) as unknown as typeof fetch
}
const PIN_KEYS = [
"ALTIMATE_CODE_SERVE",
"ALTIMATE_PINNED_WORKSPACE_ID",
"ALTIMATE_PINNED_WORKSPACE_NAME",
"ALTIMATE_PINNED_WORKSPACE_ROOT",
] as const
async function withEnv<T>(env: Partial<Record<(typeof PIN_KEYS)[number], string>>, fn: () => Promise<T>) {
const saved = Object.fromEntries(PIN_KEYS.map((k) => [k, process.env[k]]))
for (const k of PIN_KEYS) delete process.env[k]
Object.assign(process.env, env)
try {
return await fn()
} finally {
for (const k of PIN_KEYS) {
if (saved[k] === undefined) delete process.env[k]
else process.env[k] = saved[k]
}
}
}

test("a _workspace folder this client did not write is not evidence of a workspace", async () => {
mkdirSync(path.join(project, MANAGED), { recursive: true })
writeFileSync(path.join(project, MANAGED, "mine.md"), "the user's own file")
unbind()
offline()
const result = await syncSkills(project)
expect(result.error).toBeUndefined()
})

test("an IDE pin for this project is evidence, so an unconfirmable pinned workspace is reported", async () => {
unbind()
offline()
const result = await withEnv(
{
ALTIMATE_CODE_SERVE: "1",
ALTIMATE_PINNED_WORKSPACE_ID: "4242",
ALTIMATE_PINNED_WORKSPACE_NAME: "pinned",
ALTIMATE_PINNED_WORKSPACE_ROOT: project,
},
() => syncSkills(project),
)
expect(result.error).toBe("could not confirm this project's workspace (offline, or no access to it)")
})

test("an unhonourable pin removes the skills AND says why", async () => {
// The pin purge removes the manifest before the evidence check runs; the
// pin itself must still count, or the purge would be silent.
serve({ "pub-1": { "SKILL.md": "one" } })
await syncSkills(project)
expect(existsSync(skillFile("pub-1", "SKILL.md"))).toBe(true)

unbind()
offline()
const result = await withEnv(
{
ALTIMATE_CODE_SERVE: "1",
ALTIMATE_PINNED_WORKSPACE_ID: "4243",
ALTIMATE_PINNED_WORKSPACE_NAME: "pinned",
ALTIMATE_PINNED_WORKSPACE_ROOT: project,
},
() => syncSkills(project),
)
expect(existsSync(skillFile("pub-1", "SKILL.md"))).toBe(false)
expect(result.error).toBe("could not confirm this project's workspace (offline, or no access to it)")
})

test("a malformed pin is evidence too: the extension set one", async () => {
unbind()
offline()
const result = await withEnv({ ALTIMATE_CODE_SERVE: "1", ALTIMATE_PINNED_WORKSPACE_ID: "4242" }, () =>
syncSkills(project),
)
expect(result.error).toBe("could not confirm this project's workspace (offline, or no access to it)")
})

test("an unusable remote id is shown sanitised and bounded, never raw", async () => {
const esc = String.fromCharCode(27)
const raw = `../evil${esc}[31m${String.fromCharCode(10)}${String.fromCharCode(0x2028)}${"x".repeat(100)}`
Expand Down
Loading