Compare commits
2 Commits
a7094c01c6
...
ff0e71da05
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff0e71da05 | ||
|
|
cbd3b95d97 |
@@ -41,13 +41,26 @@ with no restart.
|
|||||||
|
|
||||||
### The state file, not `opencode.json`
|
### 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>`.
|
> **Reason 2 is not an argument for this design over the alternative.** A
|
||||||
`opencode.json` is a mode-0644 file people paste into bug reports;
|
> `type: "local"` entry running `mcpctl mcp -p <project>` — the same stdio
|
||||||
`~/.mcpctl/opencode-state.json` is 0600, like every other mcpctl credential.
|
> bridge `config claude` uses — needs no bearer token at all, because the bridge
|
||||||
2. **The restart.** A config file is read at startup. Re-pointing the mount
|
> reads your own `~/.mcpctl/credentials`. So "no secret in a 0644 file" is not a
|
||||||
through the running server's MCP API is what makes `/mcpctl` instant.
|
> 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
|
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
|
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 [
|
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: {
|
languageOptions: {
|
||||||
parser: tsparser,
|
parser: tsparser,
|
||||||
parserOptions: {
|
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'");
|
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> {
|
async function readState(): Promise<OpencodeState> {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ function statePath(): string {
|
|||||||
|
|
||||||
async function readState(): Promise<OpencodeState> {
|
async function readState(): Promise<OpencodeState> {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user