antechamber 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +24 -14
- package/bin/opencode-antechamber +14 -1
- package/bin/opencode-pi +11 -1
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +51 -33
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/serve.d.ts +2 -1
- package/dist/commands/setup.d.ts +2 -3
- package/dist/commands/setup.js +16 -4
- package/dist/http.d.ts +8 -1
- package/dist/http.js +42 -19
- package/dist/pi-event-translation.d.ts +10 -6
- package/dist/pi-event-translation.js +237 -80
- package/dist/pi-rpc.d.ts +20 -2
- package/dist/pi-rpc.js +96 -29
- package/dist/prompt-parts.d.ts +40 -0
- package/dist/prompt-parts.js +157 -0
- package/dist/route-services.d.ts +45 -2
- package/dist/route-services.js +70 -3
- package/dist/routes-v1.js +113 -65
- package/dist/routes-v2.js +407 -95
- package/dist/server.d.ts +9 -1
- package/dist/server.js +109 -9
- package/dist/session-commands.d.ts +12 -15
- package/dist/session-commands.js +54 -13
- package/dist/session-diff.d.ts +1 -1
- package/dist/session-diff.js +10 -2
- package/dist/session-history.d.ts +1 -1
- package/dist/session-history.js +110 -44
- package/dist/session-hosts.js +2 -2
- package/dist/session-store.d.ts +8 -4
- package/dist/session-store.js +191 -38
- package/dist/session-tree.d.ts +10 -2
- package/dist/session-tree.js +55 -26
- package/dist/sessions.d.ts +32 -5
- package/dist/sessions.js +331 -120
- package/dist/sse.d.ts +16 -6
- package/dist/sse.js +77 -21
- package/dist/subagents.d.ts +5 -0
- package/dist/subagents.js +84 -71
- package/dist/tool-map.d.ts +3 -2
- package/dist/types.d.ts +152 -1
- package/dist/v2.d.ts +18 -4
- package/dist/v2.js +128 -44
- package/dist/vcs.d.ts +5 -0
- package/dist/vcs.js +199 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/omo-dag/README.md +2 -0
- package/extensions/omo-dag/docs/DESIGN.md +10 -1
- package/extensions/omo-dag/panel/index.html +1 -1
- package/extensions/omo-dag/panel/main.js +60 -60
- package/extensions/omo-dag/service/main.js +72 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,51 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-10-15
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- fix(session): accept OmO messages stored as plain-string content when titling, binding and restoring sessions instead of crashing
|
|
15
|
+
- fix(api): answer OpenChamber's location eviction (`DELETE /api/debug/location`) with OpenCode's empty 204
|
|
16
|
+
- fix(api): answer the v2 routes OpenChamber 2.3.2 calls instead of a generic 404/501. Plugin list and check return an empty inventory, `/api/experimental/generate` returns OpenCode's typed 503, and plugin update, credential update/activate, integration connections and session generate/background/move return `UnsupportedError`
|
|
17
|
+
- fix(api): stop faking mutations. `POST /api/credential` and `PATCH /api/project/{projectID}` return `UnsupportedError` instead of keeping an in-memory credential or echoing an unsaved project
|
|
18
|
+
- fix(api): reject missing or empty prompt and synthetic-context input with a typed 400 before anything reaches OmO
|
|
19
|
+
- fix(vcs): back `/api/vcs/status`, `/api/vcs/diff`, `/api/vcs/branch` and `/api/vcs/base` with read-only git instead of fixed empty data
|
|
20
|
+
- fix(cli): refuse `opencode-antechamber upgrade` with the antechamber upgrade command, and document OpenChamber 2.3.2's OpenCode update notice
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- refactor: use `findLast` for last-match scans
|
|
25
|
+
- docs(api): unknown routes return 404 on both APIs; only unknown subpaths of an existing v2 session return 501
|
|
26
|
+
|
|
27
|
+
## [0.2.0] - 2026-10-14
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- feat(api): add v2 credential, integration, file-search, VCS, and synthetic-context routes
|
|
32
|
+
- feat(events): replay retained SSE events and expose v2 shell, compaction, and execution terminals
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- fix(session): preserve rich prompt parts, metadata, ancestry, and part identities across reloads
|
|
37
|
+
- fix(runtime): retain operation ordering after caller cancellation and finalize interrupted tool cards
|
|
38
|
+
- fix(rpc): close failed transports and reject responses correlated to the wrong command
|
|
39
|
+
- fix(http): allow passwordless loopback origins across ports while retaining explicit origin checks
|
|
40
|
+
- fix(security): validate registry records and write registry state with private permissions
|
|
41
|
+
- fix(cli): advertise OpenCode API compatibility as 2.0.20
|
|
42
|
+
- fix(api): return OpenCode 2.x bootstrap shapes for `/api/config`, `/api/shell`, `/api/websearch/provider` and `GET /api/mcp` so OpenChamber 2.3.2 can initialize workspaces
|
|
43
|
+
- fix(api): answer v2 synthetic context admission with the admitted record and return the bare interrupt flag, so OpenChamber 2.3.2 can send prompts and stop turns
|
|
44
|
+
- fix(extension): bound graph responses and display omitted-node counts
|
|
45
|
+
- fix(api): carry v2 prompt attachments, agent mentions and metadata into OmO's input, admission events and message history, and answer skill references with OpenCode's tagged not-found error
|
|
46
|
+
- fix(session): admit synthetic context and v1 `noReply` messages into OmO's conversation without a turn, keep them after restarts even when no prompt follows, and stop context from titling sessions
|
|
47
|
+
- fix(events): keep an unscoped v2 `/api/event` stream global, as OpenChamber 2.3.2 subscribes
|
|
48
|
+
- fix(rpc): after the agent exits during shutdown, kill only re-identified members of its process group
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- docs(api): document route statuses, response envelopes, stream framing, and unsupported capabilities
|
|
53
|
+
- docs(maintainers): correct smoke paths, typecheck commands, and release guidance
|
|
54
|
+
|
|
10
55
|
## [0.1.1] - 2026-10-14
|
|
11
56
|
|
|
12
57
|
### Added
|
package/README.md
CHANGED
|
@@ -55,15 +55,15 @@ another executable (see [docs/configuration.md](docs/configuration.md)).
|
|
|
55
55
|
|
|
56
56
|
| OpenChamber | API | Status |
|
|
57
57
|
|---|---|---|
|
|
58
|
-
| >= 2.0.4 | v2 (`@opencode-ai/sdk/v2`) | primary target; antechamber advertises `opencode v2.0.
|
|
58
|
+
| >= 2.0.4 | v2 (`@opencode-ai/sdk/v2`) | primary target; antechamber advertises `opencode v2.0.20` on `--version` |
|
|
59
59
|
| 1.24.x | v1 | supported |
|
|
60
60
|
| other versions | - | not tested |
|
|
61
61
|
|
|
62
62
|
OpenChamber changes which OpenCode API calls it makes between releases. If a
|
|
63
63
|
feature breaks on a newer OpenChamber, run `npm run smoke` to see which check
|
|
64
64
|
failed, then open an issue with the output. See
|
|
65
|
-
[docs/api-surface.md](docs/api-surface.md) for the
|
|
66
|
-
|
|
65
|
+
[docs/api-surface.md](docs/api-surface.md) for the maintained route, response,
|
|
66
|
+
and event-stream inventory.
|
|
67
67
|
|
|
68
68
|
## Quick start
|
|
69
69
|
|
|
@@ -76,10 +76,6 @@ antechamber doctor
|
|
|
76
76
|
antechamber setup
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
The first npm publication (0.1.0) is part of the
|
|
80
|
-
[release process](docs/releasing.md). If it is not on the registry yet, use
|
|
81
|
-
[From source](#from-source) below.
|
|
82
|
-
|
|
83
79
|
`setup` updates OpenChamber's `opencodeBinary`, preserves all other settings,
|
|
84
80
|
and backs up an existing settings file before changing it. Restart OpenChamber
|
|
85
81
|
after setup. Preview changes with `antechamber setup --dry-run`.
|
|
@@ -137,7 +133,7 @@ antechamber serve --hostname 127.0.0.1 --port 4096
|
|
|
137
133
|
| `antechamber serve` | OpenCode-compatible server; `--hostname`/`--host`, `--port`, `--directory`. Bare `antechamber` also starts the server. |
|
|
138
134
|
| `antechamber setup` | Configure OpenChamber; `--settings <file>`, `--profile <dir>`, `--binary <path>`, `--dry-run`, `--systemd [unit]`, `--pi-binary <path>`, `--restart`. |
|
|
139
135
|
| `antechamber doctor` | Read-only Node, backend, agent/state directories, teardown, OpenChamber and auth checks; `--json`, `--settings <file>`, `--profile <dir>`. Exits 1 on any FAIL; warnings do not fail. |
|
|
140
|
-
| `antechamber version` | Package version plus OpenCode API contract version. `--version`/`-v` deliberately print exactly `opencode v2.0.
|
|
136
|
+
| `antechamber version` | Package version plus OpenCode API contract version. `--version`/`-v` deliberately print exactly `opencode v2.0.20` for OpenChamber. |
|
|
141
137
|
| `antechamber help` | Usage and options; also `--help`/`-h`. |
|
|
142
138
|
|
|
143
139
|
## Upgrade, uninstall and rollback
|
|
@@ -151,6 +147,13 @@ Restart OpenChamber, or `systemctl --user restart openchamber.service` for a
|
|
|
151
147
|
managed service. Run `antechamber doctor` again. If the executable moved,
|
|
152
148
|
rerun `antechamber setup`.
|
|
153
149
|
|
|
150
|
+
OpenChamber 2.3.2 may announce a newer OpenCode release. antechamber reports
|
|
151
|
+
OpenCode API 2.0.20, the minimum OpenChamber accepts, and OpenChamber offers
|
|
152
|
+
no signal that keeps that check while hiding the notice. Dismiss it or turn off
|
|
153
|
+
OpenCode update notifications in OpenChamber. Its Update action runs
|
|
154
|
+
`opencode-antechamber upgrade`, which refuses; upgrade antechamber with the
|
|
155
|
+
commands above.
|
|
156
|
+
|
|
154
157
|
To roll back wiring, copy the backup printed by setup over the settings file:
|
|
155
158
|
|
|
156
159
|
```bash
|
|
@@ -218,9 +221,11 @@ extension, and refuses it as an invalid manifest. Details:
|
|
|
218
221
|
|
|
219
222
|
## Configuration
|
|
220
223
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
+
Connector settings use the `ANTECHAMBER_` prefix; the deprecated `OCPI_`
|
|
225
|
+
prefix is still accepted as a fallback. RPC capability passthrough also reads
|
|
226
|
+
`OMO_RPC_CLIENT_CAPABILITIES` and `SENPI_RPC_CLIENT_CAPABILITIES` directly.
|
|
227
|
+
Full reference, defaults, and units:
|
|
228
|
+
[docs/configuration.md](docs/configuration.md).
|
|
224
229
|
|
|
225
230
|
| Variable | Purpose |
|
|
226
231
|
|---|---|
|
|
@@ -231,6 +236,7 @@ deployments. Full reference, defaults, and units: [docs/configuration.md](docs/c
|
|
|
231
236
|
| `ANTECHAMBER_IDLE_TIMEOUT_MS` | idle session reap timeout |
|
|
232
237
|
| `ANTECHAMBER_MAX_LIVE` | cap on concurrent live agent processes |
|
|
233
238
|
| `ANTECHAMBER_DEBUG` / `ANTECHAMBER_DEBUG_LOG` | request/event debug logging |
|
|
239
|
+
| `OMO_RPC_CLIENT_CAPABILITIES` / `SENPI_RPC_CLIENT_CAPABILITIES` | inherited RPC capabilities merged for the agent process |
|
|
234
240
|
|
|
235
241
|
## Security
|
|
236
242
|
|
|
@@ -244,8 +250,11 @@ Hardening controls (full reference in [docs/configuration.md](docs/configuration
|
|
|
244
250
|
- Optional HTTP Basic auth, matching OpenCode's own contract:
|
|
245
251
|
`OPENCODE_SERVER_PASSWORD` (plus `OPENCODE_SERVER_USERNAME`, default
|
|
246
252
|
`opencode`).
|
|
247
|
-
- Without a password,
|
|
248
|
-
|
|
253
|
+
- Without a password, browser origins are limited to the adapter's own origin,
|
|
254
|
+
HTTP(S) loopback origins (including a different loopback port), and origins
|
|
255
|
+
listed in `ANTECHAMBER_ALLOWED_ORIGINS`; other origins get `403`. With a
|
|
256
|
+
password, other origins may make authenticated requests but receive no CORS
|
|
257
|
+
headers unless explicitly allowed.
|
|
249
258
|
- Binding to a non-loopback host requires a password, `ANTECHAMBER_ALLOW_INSECURE=1`,
|
|
250
259
|
or the `--insecure-no-auth` flag.
|
|
251
260
|
- Request bodies are capped by `ANTECHAMBER_MAX_BODY_BYTES` (default 10 MiB);
|
|
@@ -268,7 +277,8 @@ of crashing; antechamber never falls back to signalling numeric PIDs. macOS is u
|
|
|
268
277
|
level, not the model level.
|
|
269
278
|
- Some OpenCode routes have no pi/OmO equivalent and are stubbed or return
|
|
270
279
|
`400`/`501` (OAuth-based provider auth, MCP connect/disconnect, session
|
|
271
|
-
share). See [docs/api-surface.md](docs/api-surface.md) for the
|
|
280
|
+
share). See [docs/api-surface.md](docs/api-surface.md) for the maintained
|
|
281
|
+
route and unsupported-response inventory.
|
|
272
282
|
|
|
273
283
|
## Development
|
|
274
284
|
|
package/bin/opencode-antechamber
CHANGED
|
@@ -4,7 +4,16 @@
|
|
|
4
4
|
# `<binary> --version`; anything else is rejected.
|
|
5
5
|
# Requires a build (`npm run build`) so that dist/cli.js exists.
|
|
6
6
|
set -eu
|
|
7
|
-
|
|
7
|
+
SCRIPT_PATH=$0
|
|
8
|
+
while [ -h "$SCRIPT_PATH" ]; do
|
|
9
|
+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
|
|
10
|
+
LINK="$(readlink "$SCRIPT_PATH")"
|
|
11
|
+
case "$LINK" in
|
|
12
|
+
/*) SCRIPT_PATH=$LINK ;;
|
|
13
|
+
*) SCRIPT_PATH="$SCRIPT_DIR/$LINK" ;;
|
|
14
|
+
esac
|
|
15
|
+
done
|
|
16
|
+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
|
|
8
17
|
CLI="$SCRIPT_DIR/../dist/cli.js"
|
|
9
18
|
if [ ! -f "$CLI" ]; then
|
|
10
19
|
echo "opencode-antechamber: $CLI not found; run 'npm run build' first" >&2
|
|
@@ -13,6 +22,10 @@ fi
|
|
|
13
22
|
case "${1:-}" in
|
|
14
23
|
serve) shift; exec node "$CLI" serve "$@" ;;
|
|
15
24
|
--version|-v) exec node "$CLI" --version ;;
|
|
25
|
+
upgrade)
|
|
26
|
+
echo "opencode-antechamber: OpenChamber cannot upgrade antechamber; run 'npm update -g antechamber' (or 'bun add -g antechamber') instead" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
;;
|
|
16
29
|
esac
|
|
17
30
|
echo "opencode-antechamber: unsupported command '${1:-}' (only 'serve' and '--version' are implemented)" >&2
|
|
18
31
|
exit 1
|
package/bin/opencode-pi
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# Deprecated name kept for existing OpenChamber settings; use opencode-antechamber.
|
|
3
|
-
|
|
3
|
+
SCRIPT_PATH=$0
|
|
4
|
+
while [ -h "$SCRIPT_PATH" ]; do
|
|
5
|
+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
|
|
6
|
+
LINK="$(readlink "$SCRIPT_PATH")"
|
|
7
|
+
case "$LINK" in
|
|
8
|
+
/*) SCRIPT_PATH=$LINK ;;
|
|
9
|
+
*) SCRIPT_PATH="$SCRIPT_DIR/$LINK" ;;
|
|
10
|
+
esac
|
|
11
|
+
done
|
|
12
|
+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
|
|
13
|
+
exec "$SCRIPT_DIR/opencode-antechamber" "$@"
|
package/dist/approvals.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type ApprovalEvent = WithoutEventID<EventPermissionAsked | EventPermissio
|
|
|
12
12
|
formAnswer?: Record<string, string | string[]>;
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
|
-
|
|
15
|
+
type PermissionReply = "once" | "always" | "reject";
|
|
16
16
|
export type PendingPermission = PermissionRequest;
|
|
17
17
|
export type PendingQuestion = QuestionRequest & {
|
|
18
18
|
questionIDs?: string[];
|
|
@@ -75,10 +75,13 @@ export declare class ApprovalBridge {
|
|
|
75
75
|
observeQuestion(ev: Record<string, unknown>): void;
|
|
76
76
|
observe(context: Context, ev: Record<string, unknown>): void;
|
|
77
77
|
private ask;
|
|
78
|
+
private askStructuredQuestion;
|
|
79
|
+
private askLegacyDialog;
|
|
78
80
|
replyPermission(id: string, reply: PermissionReply, message?: string, sessionID?: string): boolean;
|
|
79
81
|
replyQuestion(id: string, answers: string[][], sessionID?: string, comment?: string): boolean | Promise<boolean>;
|
|
80
82
|
rejectQuestion(id: string, sessionID?: string): boolean | Promise<boolean>;
|
|
81
83
|
private respondStructured;
|
|
84
|
+
private rememberSettled;
|
|
82
85
|
private follow;
|
|
83
86
|
private remove;
|
|
84
87
|
clear(sessionID: string, cancel: boolean, callID?: string): void;
|
package/dist/approvals.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
export class ApprovalInputError extends Error {
|
|
3
3
|
}
|
|
4
|
+
const MAX_SETTLED_STRUCTURED_QUESTIONS = 256;
|
|
4
5
|
function record(value) {
|
|
5
6
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
6
7
|
? value
|
|
@@ -169,7 +170,7 @@ export class ApprovalBridge {
|
|
|
169
170
|
const p = [...this.pending.values()].find((p) => p.request.method === "question" &&
|
|
170
171
|
(p.request.id === ev.id || (typeof ev.requestId === "string" && p.request.requestId === ev.requestId)));
|
|
171
172
|
if (ev.type === "question_resolved" && typeof ev.requestId === "string")
|
|
172
|
-
this.
|
|
173
|
+
this.rememberSettled(ev.requestId);
|
|
173
174
|
if (!p || p.kind !== "question")
|
|
174
175
|
return;
|
|
175
176
|
if (ev.type === "question_updated") {
|
|
@@ -181,7 +182,7 @@ export class ApprovalBridge {
|
|
|
181
182
|
p.card.metadata = { ...p.card.metadata, deadlineAtMs: p.request.deadlineAtMs, draft: p.request.draft };
|
|
182
183
|
return;
|
|
183
184
|
}
|
|
184
|
-
this.
|
|
185
|
+
this.rememberSettled(p.request.requestId);
|
|
185
186
|
if (ev.outcome !== "answered" && ev.outcome !== "comment-submitted") {
|
|
186
187
|
this.remove(p, true);
|
|
187
188
|
return;
|
|
@@ -264,41 +265,47 @@ export class ApprovalBridge {
|
|
|
264
265
|
if (!request)
|
|
265
266
|
return;
|
|
266
267
|
if (request.method === "question") {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
const card = {
|
|
286
|
-
id: publicID,
|
|
287
|
-
sessionID: context.sessionID,
|
|
288
|
-
questions: (request.questions ?? []).map(({ id: _id, ...q }) => q),
|
|
289
|
-
questionIDs: (request.questions ?? []).map((q) => q.id),
|
|
290
|
-
metadata: {
|
|
268
|
+
this.askStructuredQuestion(context, request);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.askLegacyDialog(context, request);
|
|
272
|
+
}
|
|
273
|
+
askStructuredQuestion(context, request) {
|
|
274
|
+
if (this.settled.has(request.requestId ?? ""))
|
|
275
|
+
return;
|
|
276
|
+
const publicID = `que_${createHash("sha256")
|
|
277
|
+
.update(JSON.stringify([context.sessionID, request.requestId]))
|
|
278
|
+
.digest("hex")
|
|
279
|
+
.slice(0, 32)}`;
|
|
280
|
+
const existing = this.pending.get(publicID);
|
|
281
|
+
if (existing) {
|
|
282
|
+
existing.request = request;
|
|
283
|
+
if (existing.kind === "question")
|
|
284
|
+
existing.card.metadata = {
|
|
291
285
|
requestId: request.requestId,
|
|
292
286
|
waitForAnswer: request.waitForAnswer,
|
|
293
287
|
deadlineAtMs: request.deadlineAtMs,
|
|
294
288
|
draft: request.draft,
|
|
295
|
-
}
|
|
296
|
-
tool: { messageID: context.messageID, callID: request.requestId ?? "" },
|
|
297
|
-
};
|
|
298
|
-
this.pending.set(publicID, { kind: "question", card, context, request });
|
|
299
|
-
this.emit(context.directory, { type: "question.asked", properties: card });
|
|
289
|
+
};
|
|
300
290
|
return;
|
|
301
291
|
}
|
|
292
|
+
const card = {
|
|
293
|
+
id: publicID,
|
|
294
|
+
sessionID: context.sessionID,
|
|
295
|
+
questions: (request.questions ?? []).map(({ id: _id, ...q }) => q),
|
|
296
|
+
questionIDs: (request.questions ?? []).map((q) => q.id),
|
|
297
|
+
metadata: {
|
|
298
|
+
requestId: request.requestId,
|
|
299
|
+
waitForAnswer: request.waitForAnswer,
|
|
300
|
+
deadlineAtMs: request.deadlineAtMs,
|
|
301
|
+
draft: request.draft,
|
|
302
|
+
},
|
|
303
|
+
tool: { messageID: context.messageID, callID: request.requestId ?? "" },
|
|
304
|
+
};
|
|
305
|
+
this.pending.set(publicID, { kind: "question", card, context, request });
|
|
306
|
+
this.emit(context.directory, { type: "question.asked", properties: card });
|
|
307
|
+
}
|
|
308
|
+
askLegacyDialog(context, request) {
|
|
302
309
|
if (this.pending.has(request.id))
|
|
303
310
|
return;
|
|
304
311
|
const calls = this.questionCalls.get(context.sessionID);
|
|
@@ -470,6 +477,17 @@ export class ApprovalBridge {
|
|
|
470
477
|
p.replying = false;
|
|
471
478
|
}
|
|
472
479
|
}
|
|
480
|
+
rememberSettled(requestID) {
|
|
481
|
+
if (!requestID)
|
|
482
|
+
return;
|
|
483
|
+
this.settled.delete(requestID);
|
|
484
|
+
this.settled.add(requestID);
|
|
485
|
+
if (this.settled.size <= MAX_SETTLED_STRUCTURED_QUESTIONS)
|
|
486
|
+
return;
|
|
487
|
+
const oldest = this.settled.values().next().value;
|
|
488
|
+
if (oldest !== undefined)
|
|
489
|
+
this.settled.delete(oldest);
|
|
490
|
+
}
|
|
473
491
|
follow(p, value, kind) {
|
|
474
492
|
const callID = p.request.toolCallId;
|
|
475
493
|
if (callID === undefined)
|
|
@@ -502,8 +520,8 @@ export class ApprovalBridge {
|
|
|
502
520
|
continue;
|
|
503
521
|
if (p.request.method === "question" && !cancel)
|
|
504
522
|
continue;
|
|
505
|
-
if (cancel
|
|
506
|
-
this.
|
|
523
|
+
if (cancel)
|
|
524
|
+
this.rememberSettled(p.request.requestId);
|
|
507
525
|
if (cancel)
|
|
508
526
|
p.context.send({ type: "extension_ui_response", id: p.request.id, cancelled: true });
|
|
509
527
|
this.remove(p, true);
|
package/dist/catalog.d.ts
CHANGED
package/dist/catalog.js
CHANGED
|
@@ -45,7 +45,7 @@ function toOCModel(m) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
export async function loadCatalog(directory) {
|
|
48
|
-
const pi = new PiRpcClient({ cwd: directory });
|
|
48
|
+
const pi = new PiRpcClient({ cwd: directory, args: ["--no-session"] });
|
|
49
49
|
try {
|
|
50
50
|
const [modelsRes, firstState] = await Promise.all([
|
|
51
51
|
pi.command({ type: "get_available_models" }),
|
|
@@ -13,7 +13,6 @@ export interface CommandContext {
|
|
|
13
13
|
readonly launcherPath: string;
|
|
14
14
|
}
|
|
15
15
|
export declare function createContext(overrides?: Partial<CommandContext>): CommandContext;
|
|
16
|
-
export declare const packageCliPath: string;
|
|
17
16
|
/** Resolve executables without shell evaluation, respecting the supplied PATH. */
|
|
18
17
|
export declare function resolveBinary(binary: string, ctx: CommandContext): string | undefined;
|
|
19
18
|
export interface SettingsOptions {
|
package/dist/commands/context.js
CHANGED
|
@@ -35,7 +35,7 @@ export function createContext(overrides = {}) {
|
|
|
35
35
|
...overrides,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
const packageCliPath = fileURLToPath(new URL("../../dist/cli.js", import.meta.url));
|
|
39
39
|
/** Resolve executables without shell evaluation, respecting the supplied PATH. */
|
|
40
40
|
export function resolveBinary(binary, ctx) {
|
|
41
41
|
const candidates = isAbsolute(binary) || binary.includes("/") || binary.includes("\\")
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { type CommandContext, type SettingsOptions } from "./context.js";
|
|
2
|
-
|
|
2
|
+
interface DoctorCheck {
|
|
3
3
|
readonly name: string;
|
|
4
4
|
readonly status: "PASS" | "WARN" | "FAIL" | "INFO";
|
|
5
5
|
readonly message: string;
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
interface DoctorReport {
|
|
8
8
|
readonly checks: readonly DoctorCheck[];
|
|
9
9
|
readonly exitCode: 0 | 1;
|
|
10
10
|
}
|
|
11
11
|
/** Checks capabilities with access/stat only, never creating state or config files. */
|
|
12
12
|
export declare function doctor(options?: SettingsOptions, ctx?: CommandContext): DoctorReport;
|
|
13
13
|
export declare function formatDoctor(report: DoctorReport, json: boolean): string;
|
|
14
|
+
export {};
|
package/dist/commands/serve.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
interface ServeOptions {
|
|
2
2
|
readonly port: number;
|
|
3
3
|
readonly host: string;
|
|
4
4
|
readonly directory: string;
|
|
@@ -6,3 +6,4 @@ export interface ServeOptions {
|
|
|
6
6
|
readonly compatible: boolean;
|
|
7
7
|
}
|
|
8
8
|
export declare function serve(options: ServeOptions): Promise<void>;
|
|
9
|
+
export {};
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CommandContext, type SettingsOptions } from "./context.js";
|
|
2
|
-
|
|
2
|
+
interface SetupOptions extends SettingsOptions {
|
|
3
3
|
readonly binary?: string;
|
|
4
4
|
readonly dryRun?: boolean;
|
|
5
5
|
readonly systemd?: string;
|
|
@@ -8,7 +8,6 @@ export interface SetupOptions extends SettingsOptions {
|
|
|
8
8
|
/** User-unit root, overridable without touching the real systemd configuration. */
|
|
9
9
|
readonly systemdDir?: string;
|
|
10
10
|
}
|
|
11
|
-
/** The bundled OmO Graph extension folder, when this installation has its built files. */
|
|
12
|
-
export declare function omoGraphExtensionDir(ctx: CommandContext): string | undefined;
|
|
13
11
|
/** Plan all inputs before applying changes; dependencies are injected for hermetic tests. */
|
|
14
12
|
export declare function setup(options: SetupOptions, ctx?: CommandContext): string[];
|
|
13
|
+
export {};
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { dirname, join, resolve } from "node:path";
|
|
2
3
|
import { createContext, isPackageBinary, readSettings, resolveBinary, settingsPath, shellQuote, } from "./context.js";
|
|
3
4
|
function systemdValue(value) {
|
|
@@ -5,8 +6,13 @@ function systemdValue(value) {
|
|
|
5
6
|
throw new Error("systemd environment values cannot contain newlines or NUL");
|
|
6
7
|
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%");
|
|
7
8
|
}
|
|
9
|
+
function writeAtomically(path, content, ctx) {
|
|
10
|
+
const temporary = join(dirname(path), `.antechamber-${randomUUID()}.tmp`);
|
|
11
|
+
ctx.fs.writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
|
|
12
|
+
ctx.fs.renameSync(temporary, path);
|
|
13
|
+
}
|
|
8
14
|
/** The bundled OmO Graph extension folder, when this installation has its built files. */
|
|
9
|
-
|
|
15
|
+
function omoGraphExtensionDir(ctx) {
|
|
10
16
|
const dir = resolve(dirname(ctx.cliPath), "..", "extensions", "omo-dag");
|
|
11
17
|
const built = ["package.json", "panel/main.js", "service/main.js"].every((file) => ctx.fs.existsSync(join(dir, file)));
|
|
12
18
|
return built ? dir : undefined;
|
|
@@ -75,8 +81,14 @@ export function setup(options, ctx = createContext()) {
|
|
|
75
81
|
const stamp = ctx.now().toISOString().replace(/\D/g, "").slice(0, 14);
|
|
76
82
|
const backup = (path) => {
|
|
77
83
|
const target = `${path}.bak-antechamber-${stamp}`;
|
|
78
|
-
if (options.dryRun)
|
|
84
|
+
if (options.dryRun) {
|
|
85
|
+
if (ctx.fs.existsSync(target)) {
|
|
86
|
+
throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${path}' -> '${target}'`), {
|
|
87
|
+
code: "EEXIST",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
79
90
|
report(`Would back up ${path} to ${target}`);
|
|
91
|
+
}
|
|
80
92
|
else {
|
|
81
93
|
ctx.fs.copyFileSync(path, target, ctx.fs.constants.COPYFILE_EXCL);
|
|
82
94
|
report(`Backed up ${path} to ${target}`);
|
|
@@ -98,7 +110,7 @@ export function setup(options, ctx = createContext()) {
|
|
|
98
110
|
backup(settings);
|
|
99
111
|
if (!options.dryRun) {
|
|
100
112
|
ctx.fs.mkdirSync(dirname(settings), { recursive: true });
|
|
101
|
-
|
|
113
|
+
writeAtomically(settings, updated, ctx);
|
|
102
114
|
report(`Wrote ${settings}`);
|
|
103
115
|
}
|
|
104
116
|
}
|
|
@@ -113,7 +125,7 @@ export function setup(options, ctx = createContext()) {
|
|
|
113
125
|
backup(dropIn);
|
|
114
126
|
if (!options.dryRun) {
|
|
115
127
|
ctx.fs.mkdirSync(dirname(dropIn), { recursive: true });
|
|
116
|
-
|
|
128
|
+
writeAtomically(dropIn, dropInText, ctx);
|
|
117
129
|
report(`Wrote ${dropIn}`);
|
|
118
130
|
}
|
|
119
131
|
}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/** Shared HTTP body, response, authentication, and CORS plumbing. */
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
export declare class RequestBodyError extends Error {
|
|
4
|
+
readonly status: 400 | 413;
|
|
5
|
+
constructor(status: 400 | 413, message: string);
|
|
6
|
+
}
|
|
3
7
|
export declare function serverPassword(): string | undefined;
|
|
4
8
|
export declare function isLoopbackHost(host: string): boolean;
|
|
5
9
|
export declare function errorBody(name: string, message: string): {
|
|
@@ -10,9 +14,12 @@ export declare function errorBody(name: string, message: string): {
|
|
|
10
14
|
};
|
|
11
15
|
export declare function json(res: ServerResponse, status: number, body: unknown): void;
|
|
12
16
|
export declare class Http {
|
|
17
|
+
private serverOrigin?;
|
|
13
18
|
private readonly credentials;
|
|
14
19
|
private readonly maxBodyBytes;
|
|
15
|
-
constructor();
|
|
20
|
+
constructor(serverOrigin?: (() => string) | undefined);
|
|
21
|
+
allowsUpgrade(req: IncomingMessage): boolean;
|
|
16
22
|
route(req: IncomingMessage, res: ServerResponse, handle: () => Promise<void>): Promise<void>;
|
|
17
23
|
body(req: IncomingMessage): Promise<unknown>;
|
|
24
|
+
private authorized;
|
|
18
25
|
}
|
package/dist/http.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import { ApprovalInputError } from "./approvals.js";
|
|
4
4
|
import { envSetting } from "./config.js";
|
|
5
|
+
import { UpstreamProtocolError } from "./pi-rpc.js";
|
|
5
6
|
const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
6
|
-
class RequestBodyError extends Error {
|
|
7
|
+
export class RequestBodyError extends Error {
|
|
7
8
|
status;
|
|
8
9
|
constructor(status, message) {
|
|
9
10
|
super(message);
|
|
@@ -19,7 +20,7 @@ export function isLoopbackHost(host) {
|
|
|
19
20
|
return true;
|
|
20
21
|
return /^127(?:\.\d{1,3}){3}$/.test(host) && host.split(".").every((part) => Number(part) <= 255);
|
|
21
22
|
}
|
|
22
|
-
function allowedOrigin(origin) {
|
|
23
|
+
function allowedOrigin(origin, serverOrigin) {
|
|
23
24
|
try {
|
|
24
25
|
const url = new URL(origin);
|
|
25
26
|
if (!/^https?:\/\/[^/?#]+$/.test(origin) ||
|
|
@@ -29,7 +30,8 @@ function allowedOrigin(origin) {
|
|
|
29
30
|
url.search !== "" ||
|
|
30
31
|
url.hash !== "")
|
|
31
32
|
return false;
|
|
32
|
-
return (
|
|
33
|
+
return (origin === serverOrigin ||
|
|
34
|
+
isLoopbackHost(url.hostname) ||
|
|
33
35
|
(envSetting("ALLOWED_ORIGINS") ?? "").split(",").some((entry) => entry.trim() === origin));
|
|
34
36
|
}
|
|
35
37
|
catch {
|
|
@@ -44,9 +46,11 @@ export function json(res, status, body) {
|
|
|
44
46
|
res.writeHead(status, { "Content-Type": "application/json" }).end(payload);
|
|
45
47
|
}
|
|
46
48
|
export class Http {
|
|
49
|
+
serverOrigin;
|
|
47
50
|
credentials;
|
|
48
51
|
maxBodyBytes;
|
|
49
|
-
constructor() {
|
|
52
|
+
constructor(serverOrigin) {
|
|
53
|
+
this.serverOrigin = serverOrigin;
|
|
50
54
|
const maxBodyBytes = Number(envSetting("MAX_BODY_BYTES"));
|
|
51
55
|
this.maxBodyBytes = Number.isSafeInteger(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
52
56
|
const password = serverPassword();
|
|
@@ -57,11 +61,17 @@ export class Http {
|
|
|
57
61
|
.update(`${process.env.OPENCODE_SERVER_USERNAME || "opencode"}:${password}`)
|
|
58
62
|
.digest();
|
|
59
63
|
}
|
|
64
|
+
allowsUpgrade(req) {
|
|
65
|
+
const origin = req.headers.origin;
|
|
66
|
+
if (origin !== undefined && !allowedOrigin(origin, this.serverOrigin?.()) && this.credentials === undefined)
|
|
67
|
+
return false;
|
|
68
|
+
return this.authorized(req);
|
|
69
|
+
}
|
|
60
70
|
async route(req, res, handle) {
|
|
61
71
|
res.setHeader("Vary", "Origin");
|
|
62
72
|
const origin = req.headers.origin;
|
|
63
73
|
if (origin !== undefined) {
|
|
64
|
-
if (allowedOrigin(origin)) {
|
|
74
|
+
if (allowedOrigin(origin, this.serverOrigin?.())) {
|
|
65
75
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
66
76
|
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
67
77
|
}
|
|
@@ -74,16 +84,10 @@ export class Http {
|
|
|
74
84
|
return;
|
|
75
85
|
}
|
|
76
86
|
}
|
|
77
|
-
if (this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.digest();
|
|
82
|
-
if (!match || !timingSafeEqual(this.credentials, supplied)) {
|
|
83
|
-
res.setHeader("WWW-Authenticate", 'Basic realm="OpenCode"');
|
|
84
|
-
json(res, 401, errorBody("UnauthorizedError", "Authentication required"));
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
+
if (!this.authorized(req)) {
|
|
88
|
+
res.setHeader("WWW-Authenticate", 'Basic realm="OpenCode"');
|
|
89
|
+
json(res, 401, errorBody("UnauthorizedError", "Authentication required"));
|
|
90
|
+
return;
|
|
87
91
|
}
|
|
88
92
|
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
|
|
89
93
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-opencode-directory");
|
|
@@ -100,11 +104,21 @@ export class Http {
|
|
|
100
104
|
res.setHeader("Connection", "close");
|
|
101
105
|
res.once("finish", () => req.destroy());
|
|
102
106
|
}
|
|
103
|
-
json(res, err instanceof
|
|
107
|
+
json(res, err instanceof UpstreamProtocolError
|
|
108
|
+
? 502
|
|
109
|
+
: err instanceof RequestBodyError
|
|
110
|
+
? err.status
|
|
111
|
+
: err instanceof ApprovalInputError || err instanceof URIError
|
|
112
|
+
? 400
|
|
113
|
+
: 500, errorBody(err instanceof UpstreamProtocolError
|
|
104
114
|
? err.name
|
|
105
|
-
: err instanceof
|
|
106
|
-
?
|
|
107
|
-
:
|
|
115
|
+
: err instanceof RequestBodyError
|
|
116
|
+
? err.name
|
|
117
|
+
: err instanceof ApprovalInputError
|
|
118
|
+
? "InvalidRequestError"
|
|
119
|
+
: err instanceof URIError
|
|
120
|
+
? "BadRequestError"
|
|
121
|
+
: "UnknownError", err instanceof URIError ? "invalid URL encoding" : message));
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
body(req) {
|
|
@@ -165,4 +179,13 @@ export class Http {
|
|
|
165
179
|
req.once("aborted", onAborted);
|
|
166
180
|
});
|
|
167
181
|
}
|
|
182
|
+
authorized(req) {
|
|
183
|
+
if (this.credentials === undefined)
|
|
184
|
+
return true;
|
|
185
|
+
const match = req.headers.authorization?.match(/^Basic\s+([A-Za-z0-9+/]+=*)$/i);
|
|
186
|
+
const supplied = createHash("sha256")
|
|
187
|
+
.update(Buffer.from(match?.[1] ?? "", "base64"))
|
|
188
|
+
.digest();
|
|
189
|
+
return match !== null && match !== undefined && timingSafeEqual(this.credentials, supplied);
|
|
190
|
+
}
|
|
168
191
|
}
|