opencode gets the skills and a credential, which it had neither of

an opencode agent could not have used the calendar skill written this morning,
and would not have failed usefully either — it had no idea the skill existed and
no key to call the platform with. two gaps, one file each.

SKILLS: OPENCODE ALREADY HAD THE MECHANISM. it has a `skill` tool, an index
compiled into its system prompt, and `skills.paths` in config taking directories
of <name>/SKILL.md — the same progressive disclosure claude does over the same
layout the platform already stores. so this points it at the SAME FILES rather
than compiling a second index in a second format. skills/load.ts says a skill is
a path because every harness can read a file; this is the better version of that
bet — the harness had the feature all along. verified: `/skill?directory=…` now
returns all nine platform skills, "Calendar and contacts" among them.

THE CREDENTIAL, AND THE THING TO KNOW ABOUT IT. claude spawns a subprocess per
session, so its key is per session and a member's resolves to their own grants.
opencode cannot: ONE serve runs every session, so the only environment there is
is the serve's own, minted once at sidecar start, belonging to the owner.

that is sound today for exactly one reason — opencode is owner-only, enforced at
every door in chat.ts because one serve runs as one user. when that lifts, this
key becomes a privilege escalation. it is the same blocker as the two already
there, not a new one, but it lives in a sidecar where nobody looking at chat.ts
would find it, so chat.ts now names it. all three move together.

verified end to end: a tool process inside an opencode session ran curl with
$OFFICER_AGENT_KEY and got the owner's calendars back.

OPENCODE_CONFIG_CONTENT rather than editing ~/.config/opencode/opencode.jsonc —
it is inline json, it MERGES into the same chain as the config files (checked in
the binary), and it dies with the process. the alternative is the platform
editing a dotfile it does not own and leaving state behind that outlives the
install.

affordances ride the `instructions` key as one composed file, written beside the
serve's state. loaded — `/config` shows it — but NOT model-verified: this box has
no provider credential in auth.json, so a turn 401s before reaching a model.
whoever adds one should check that first.

the credential module moved out of sidecar/claude/. it was never claude-shaped;
it was just there first, and it had one importer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-27 11:13:00 +00:00
co-authored by Claude Opus 5
parent 0918dd7443
commit e6da5d8c77
6 changed files with 175 additions and 3 deletions
@@ -0,0 +1,27 @@
import { describe, expect, test } from 'bun:test';
import { serveConfigContent, affordancesPath } from './serve-env';
// The config shape is a contract with the OpenCode binary, and the cost of a wrong key name is SILENCE:
// no error, no warning, just an agent that never sees a skill. So the keys are asserted literally.
describe('serveConfigContent', () => {
test('names skills.paths and instructions exactly', () => {
const parsed = JSON.parse(serveConfigContent(['/a/skills', '/b/skills'], ['/tmp/aff.md']));
expect(parsed).toEqual({ skills: { paths: ['/a/skills', '/b/skills'] }, instructions: ['/tmp/aff.md'] });
});
// An empty array is not the same as an absent key: `skills: {paths: []}` is a claim that there are none,
// and merged over a config that sets some it would erase them.
test('omits a key rather than sending it empty', () => {
expect(JSON.parse(serveConfigContent([], []))).toEqual({});
expect(JSON.parse(serveConfigContent(['/a'], []))).toEqual({ skills: { paths: ['/a'] } });
});
test('is valid JSON for an env var — no newlines to be mangled by a shell', () => {
expect(serveConfigContent(['/a/skills'], ['/tmp/aff.md'])).not.toContain('\n');
});
test('the affordances file sits beside the serve state, not in a home', () => {
expect(affordancesPath('/data/opencode_server')).toBe('/data/opencode_server/officer-affordances.md');
});
});