Compare commits
2 Commits
a7094c01c6
...
ff0e71da05
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff0e71da05 | ||
|
|
cbd3b95d97 |
@@ -41,13 +41,26 @@ with no restart.
|
||||
|
||||
### The state file, not `opencode.json`
|
||||
|
||||
Two reasons the project does not live in opencode's own config:
|
||||
1. **The restart.** A config file is read at startup. Re-pointing the mount
|
||||
through the running server's MCP API is what makes `/mcpctl` instant. This is
|
||||
the load-bearing reason.
|
||||
2. **The token.** Having chosen the gateway, we need
|
||||
`Authorization: Bearer <mcpctl PAT>` somewhere. `opencode.json` is a mode-0644
|
||||
file people paste into bug reports; `~/.mcpctl/opencode-state.json` is 0600,
|
||||
like every other mcpctl credential.
|
||||
|
||||
1. **The token.** The gateway needs `Authorization: Bearer <mcpctl PAT>`.
|
||||
`opencode.json` is a mode-0644 file people paste into bug reports;
|
||||
`~/.mcpctl/opencode-state.json` is 0600, like every other mcpctl credential.
|
||||
2. **The restart.** A config file is read at startup. Re-pointing the mount
|
||||
through the running server's MCP API is what makes `/mcpctl` instant.
|
||||
> **Reason 2 is not an argument for this design over the alternative.** A
|
||||
> `type: "local"` entry running `mcpctl mcp -p <project>` — the same stdio
|
||||
> bridge `config claude` uses — needs no bearer token at all, because the bridge
|
||||
> reads your own `~/.mcpctl/credentials`. So "no secret in a 0644 file" is not a
|
||||
> point against that approach; it is just a consequence of having picked the
|
||||
> HTTP gateway.
|
||||
>
|
||||
> The honest trade is: the gateway works against a remote mcpctl with no local
|
||||
> `mcplocal` daemon, and mounts through an API that can be re-pointed live. The
|
||||
> stdio bridge is simpler and credential-free, but requires `mcpctl` and a
|
||||
> reachable mcplocal on the same machine. Both are defensible; this one was
|
||||
> chosen for the remote case and for the live re-point, not for the token.
|
||||
|
||||
Tokens are kept **per project**, so switching back to a project you have
|
||||
already used needs no new mint — and a failed mint for project B cannot cost
|
||||
|
||||
@@ -3,7 +3,7 @@ import tsparser from '@typescript-eslint/parser';
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ['src/*/src/**/*.ts', 'src/pi-ext/*.ts', 'src/opencode-ext/*.ts', 'src/prime-agent-ext/*.ts'],
|
||||
files: ['src/*/src/**/*.ts', 'src/pi-ext/*.ts', 'src/opencode-ext/*.ts', 'src/opencode-ext/*.tsx', 'src/prime-agent-ext/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -88,3 +88,16 @@ describe('embedded opencode plugins', () => {
|
||||
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("'--skip-marker'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('embedded opencode plugins — state parsing', () => {
|
||||
it('type-guard the parsed state, not just try/catch', () => {
|
||||
// `JSON.parse('null')` succeeds and returns null, so a bare try/catch lets
|
||||
// it through and the next `state.project` throws a TypeError that takes the
|
||||
// plugin down. A hand-edited or truncated state file must degrade to "no
|
||||
// project", never to a broken opencode.
|
||||
for (const src of [OPENCODE_SERVER_PLUGIN_SOURCE, OPENCODE_TUI_PLUGIN_SOURCE]) {
|
||||
expect(src).toContain("typeof parsed === 'object' && parsed !== null");
|
||||
expect(src).not.toMatch(/return JSON\.parse\(await readFile\([^)]*\)\) as OpencodeState;/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,12 @@ function statePath(): string {
|
||||
|
||||
async function readState(): Promise<OpencodeState> {
|
||||
try {
|
||||
return JSON.parse(await readFile(statePath(), 'utf-8')) as OpencodeState;
|
||||
const parsed: unknown = JSON.parse(await readFile(statePath(), 'utf-8'));
|
||||
// Type-guard, not just try/catch: `JSON.parse('null')` succeeds and returns
|
||||
// null, so the catch never fires and the next `state.project` throws a
|
||||
// TypeError that takes the plugin down. A truncated or hand-edited state
|
||||
// file must degrade to "no project", never to a broken opencode.
|
||||
return typeof parsed === 'object' && parsed !== null ? (parsed as OpencodeState) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -40,7 +40,12 @@ function statePath(): string {
|
||||
|
||||
async function readState(): Promise<OpencodeState> {
|
||||
try {
|
||||
return JSON.parse(await readFile(statePath(), 'utf-8')) as OpencodeState;
|
||||
const parsed: unknown = JSON.parse(await readFile(statePath(), 'utf-8'));
|
||||
// Type-guard, not just try/catch: `JSON.parse('null')` succeeds and returns
|
||||
// null, so the catch never fires and the next `state.project` throws a
|
||||
// TypeError that takes the plugin down. A truncated or hand-edited state
|
||||
// file must degrade to "no project", never to a broken opencode.
|
||||
return typeof parsed === 'object' && parsed !== null ? (parsed as OpencodeState) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user