@celestea/core 2.7.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/LICENSE +21 -0
- package/README.md +95 -0
- package/contracts/data-files/checkpoint.schema.json +111 -0
- package/contracts/data-files/cli-main-jsonl-precompact.schema.json +27 -0
- package/contracts/data-files/cli-main-jsonl.schema.json +22 -0
- package/contracts/data-files/fallbacks.schema.json +71 -0
- package/contracts/data-files/index.json +124 -0
- package/contracts/data-files/pricing.schema.json +65 -0
- package/contracts/data-files/prompts.schema.json +130 -0
- package/contracts/data-files/providers.schema.json +177 -0
- package/contracts/data-files/registry-tsv.schema.json +74 -0
- package/contracts/data-files/session.schema.json +51 -0
- package/contracts/data-files/usage-ledger.schema.json +112 -0
- package/contracts/data-files/workspaces.schema.json +63 -0
- package/contracts/endpoints.json +4390 -0
- package/contracts/probe-evidence.json +219 -0
- package/contracts/route-table.snapshot.json +377 -0
- package/contracts/scope-hash-vectors.json +273 -0
- package/contracts/session-event.schema.json +441 -0
- package/contracts/sse-events.json +202 -0
- package/contracts/tools.json +730 -0
- package/dist/agent.d.ts +65 -0
- package/dist/agent.js +36 -0
- package/dist/celestea-home.d.ts +63 -0
- package/dist/celestea-home.js +96 -0
- package/dist/celestea-sources.d.ts +53 -0
- package/dist/celestea-sources.js +61 -0
- package/dist/context.d.ts +33 -0
- package/dist/context.js +55 -0
- package/dist/contracts/index.d.ts +234 -0
- package/dist/contracts/index.js +159 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +22 -0
- package/dist/event-bus.d.ts +60 -0
- package/dist/event-bus.js +100 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +66 -0
- package/dist/injection.d.ts +61 -0
- package/dist/injection.js +27 -0
- package/dist/json.d.ts +34 -0
- package/dist/json.js +127 -0
- package/dist/llm.d.ts +34 -0
- package/dist/llm.js +41 -0
- package/dist/memory.d.ts +72 -0
- package/dist/memory.js +123 -0
- package/dist/message.d.ts +189 -0
- package/dist/message.js +252 -0
- package/dist/plugin.d.ts +38 -0
- package/dist/plugin.js +49 -0
- package/dist/projection.d.ts +67 -0
- package/dist/projection.js +168 -0
- package/dist/question.d.ts +154 -0
- package/dist/question.js +82 -0
- package/dist/redact.d.ts +40 -0
- package/dist/redact.js +185 -0
- package/dist/repo.d.ts +14 -0
- package/dist/repo.js +87 -0
- package/dist/sandbox.d.ts +182 -0
- package/dist/sandbox.js +78 -0
- package/dist/session-event.d.ts +57 -0
- package/dist/session-event.js +425 -0
- package/dist/session-log.d.ts +71 -0
- package/dist/session-log.js +66 -0
- package/dist/skill-catalog.d.ts +29 -0
- package/dist/skill-catalog.js +52 -0
- package/dist/skills.d.ts +116 -0
- package/dist/skills.js +273 -0
- package/dist/sse-bus.d.ts +40 -0
- package/dist/sse-bus.js +105 -0
- package/dist/stream.d.ts +115 -0
- package/dist/stream.js +52 -0
- package/dist/tool-surface.d.ts +45 -0
- package/dist/tool-surface.js +98 -0
- package/dist/tool.d.ts +77 -0
- package/dist/tool.js +15 -0
- package/dist/turn-id.d.ts +37 -0
- package/dist/turn-id.js +76 -0
- package/dist/types.d.ts +396 -0
- package/dist/types.js +58 -0
- package/package.json +27 -0
package/dist/repo.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Locate the repository root (the directory holding pnpm-workspace.yaml). */
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
let cached = null;
|
|
6
|
+
/**
|
|
7
|
+
* H (packaging): contracts shipped INSIDE this package, resolved from the
|
|
8
|
+
* module's own location regardless of cwd.
|
|
9
|
+
*
|
|
10
|
+
* In a source checkout this file is `packages/core/src/repo.ts` / `dist/repo.js`,
|
|
11
|
+
* so `<pkg>/contracts` sits one level up from `src`/`dist`. After
|
|
12
|
+
* `npm i -g celestea-agent` the same relative walk finds
|
|
13
|
+
* `@celestea/core/contracts` (shipped via `files`), so the frozen contract
|
|
14
|
+
* files travel with the code instead of relying on the git checkout layout.
|
|
15
|
+
*/
|
|
16
|
+
function packagedContractsRoot(from) {
|
|
17
|
+
let dir = dirname(fileURLToPath(from));
|
|
18
|
+
for (let i = 0; i < 8; i++) {
|
|
19
|
+
const candidate = resolve(dir, "contracts");
|
|
20
|
+
// The marker is the CONTRACT DATA file, not the directory name: the source
|
|
21
|
+
// tree has a `src/contracts/` TS module directory, which must never win.
|
|
22
|
+
if (existsSync(resolve(candidate, "endpoints.json")))
|
|
23
|
+
return candidate;
|
|
24
|
+
const parent = dirname(dir);
|
|
25
|
+
if (parent === dir)
|
|
26
|
+
break;
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** The checkout root (the dir holding `pnpm-workspace.yaml`), or null. */
|
|
32
|
+
function workspaceMarkerRoot(from) {
|
|
33
|
+
let dir = dirname(fileURLToPath(from));
|
|
34
|
+
for (let i = 0; i < 12; i++) {
|
|
35
|
+
if (existsSync(resolve(dir, "pnpm-workspace.yaml")))
|
|
36
|
+
return dir;
|
|
37
|
+
const parent = dirname(dir);
|
|
38
|
+
if (parent === dir)
|
|
39
|
+
break;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
export function repoRoot(from = import.meta.url) {
|
|
45
|
+
if (cached)
|
|
46
|
+
return cached;
|
|
47
|
+
const workspace = workspaceMarkerRoot(from);
|
|
48
|
+
if (workspace !== null) {
|
|
49
|
+
cached = workspace;
|
|
50
|
+
return workspace;
|
|
51
|
+
}
|
|
52
|
+
// H: an INSTALLED package has no workspace marker. Fall back to the
|
|
53
|
+
// package-local contracts root's parent (still never the cwd), so a globally
|
|
54
|
+
// installed `celestea` boots the same way the checkout does.
|
|
55
|
+
const packaged = packagedContractsRoot(from);
|
|
56
|
+
if (packaged !== null) {
|
|
57
|
+
cached = dirname(packaged);
|
|
58
|
+
return cached;
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`repository root not found above ${from}`);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Absolute path to the frozen `contracts/` directory.
|
|
64
|
+
*
|
|
65
|
+
* H: a SOURCE CHECKOUT always uses `<repo>/contracts` — the build-staged
|
|
66
|
+
* `packages/core/contracts/` is a shipping artifact and must never shadow a
|
|
67
|
+
* live edit of the repo's contracts in dev. Only an INSTALLED package (no
|
|
68
|
+
* workspace marker) reads its own bundled `contracts/`.
|
|
69
|
+
*/
|
|
70
|
+
export function contractsDir() {
|
|
71
|
+
const workspace = workspaceMarkerRoot(import.meta.url);
|
|
72
|
+
if (workspace !== null)
|
|
73
|
+
return resolve(workspace, "contracts");
|
|
74
|
+
const packaged = packagedContractsRoot(import.meta.url);
|
|
75
|
+
if (packaged !== null)
|
|
76
|
+
return packaged;
|
|
77
|
+
return resolve(repoRoot(), "contracts");
|
|
78
|
+
}
|
|
79
|
+
export function contractPath(...parts) {
|
|
80
|
+
return resolve(contractsDir(), ...parts);
|
|
81
|
+
}
|
|
82
|
+
export function fixturePath(...parts) {
|
|
83
|
+
return resolve(repoRoot(), "fixtures", ...parts);
|
|
84
|
+
}
|
|
85
|
+
export function reportPath(...parts) {
|
|
86
|
+
return resolve(repoRoot(), "reports", ...parts);
|
|
87
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox seam — the injected execution boundary behind `run_shell`.
|
|
3
|
+
*
|
|
4
|
+
* Parity target: `crates/tools/src/sandbox.rs` (`SandboxConfig`, `SandboxMeta`,
|
|
5
|
+
* `SandboxOutput`, `SandboxError`, `execute_sandboxed`, `spawn_sandboxed`) plus
|
|
6
|
+
* the `OsSandboxLayer` hook that lets an OS-level layer (bubblewrap / raw
|
|
7
|
+
* namespaces / seccomp) wrap the direct command without touching call sites.
|
|
8
|
+
*
|
|
9
|
+
* Why a seam in `core`: `run_shell` must be pure orchestration (argument
|
|
10
|
+
* handling, timeout/cap bookkeeping, background hand-off) and must never know
|
|
11
|
+
* *how* isolation is achieved. Implementations are plugins: P2b ships the
|
|
12
|
+
* userspace implementation in `@celestea/tools`; the real OS isolation lands in
|
|
13
|
+
* P2c behind this exact interface (ARCHITECTURE.md §3.1, §7.4).
|
|
14
|
+
*
|
|
15
|
+
* Invariants kept from the legacy engine:
|
|
16
|
+
* - the effective isolation mode travels *inside* every result (`SandboxMeta`):
|
|
17
|
+
* a caller never infers the isolation level, and silent degradation stays
|
|
18
|
+
* visible;
|
|
19
|
+
* - every failure is a structured `SandboxError` — `run_shell-sandbox: code=<k>
|
|
20
|
+
* msg="<quoted>"` — never a bare string, never a thrown non-Error;
|
|
21
|
+
* - a kill deadline is enforced for foreground runs; background runs carry no
|
|
22
|
+
* call-level deadline (they outlive the turn) and are reaped by the caller.
|
|
23
|
+
*/
|
|
24
|
+
import type { Readable, Writable } from "node:stream";
|
|
25
|
+
/** Effective isolation mode of one run ("bwrap" | "raw" | "userspace" | …). */
|
|
26
|
+
export interface SandboxMeta {
|
|
27
|
+
/** Provider that actually executed the command. */
|
|
28
|
+
provider: string;
|
|
29
|
+
/** W6: effective `RLIMIT_CPU` in seconds for this run (absent = not reported). */
|
|
30
|
+
cpu_sec?: number;
|
|
31
|
+
/** true when the child ran in an isolated network namespace. */
|
|
32
|
+
net_isolated: boolean;
|
|
33
|
+
/** true when /tmp was a sandbox-private tmpfs. */
|
|
34
|
+
tmp_private: boolean;
|
|
35
|
+
/** true when a seccomp syscall whitelist was applied. */
|
|
36
|
+
seccomp: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** The userspace (no OS isolation) mode: everything reported, nothing hidden. */
|
|
39
|
+
export declare const USERSPACE_SANDBOX_META: SandboxMeta;
|
|
40
|
+
/** Tuning knobs every sandbox implementation honours (`SandboxConfig`). */
|
|
41
|
+
export interface SandboxConfig {
|
|
42
|
+
/** Kill deadline when the call passes no `timeoutMs`. */
|
|
43
|
+
timeoutMs: number;
|
|
44
|
+
/** Upper bound accepted for a per-call `timeoutMs`. */
|
|
45
|
+
maxTimeoutMs: number;
|
|
46
|
+
/** W6: upper bound accepted for a per-call `cpuSec` (env `CELESTEA_SHELL_MAX_CPU_SEC`). */
|
|
47
|
+
maxCpuSec: number;
|
|
48
|
+
/** Per-stream (stdout / stderr) capture cap in bytes. */
|
|
49
|
+
maxOutputBytes: number;
|
|
50
|
+
/** Fixed workdir: the default cwd of every command. */
|
|
51
|
+
workdir: string;
|
|
52
|
+
/** Canonical prefix every resolved workdir must stay inside. */
|
|
53
|
+
root: string;
|
|
54
|
+
/**
|
|
55
|
+
* W880: absolute directory the `run_code` broker writes its transient program
|
|
56
|
+
* files into. It lives OUTSIDE the workspace now (under `CELESTEA_HOME`), so
|
|
57
|
+
* the bwrap provider must bind it into the namespace or the child cannot read
|
|
58
|
+
* the program it was told to run.
|
|
59
|
+
*/
|
|
60
|
+
programDir: string;
|
|
61
|
+
/** Deliberate operator env injected on top of the allowlist. */
|
|
62
|
+
extraEnv: ReadonlyArray<readonly [string, string]>;
|
|
63
|
+
}
|
|
64
|
+
/** Terminal state of a child process. */
|
|
65
|
+
export interface SandboxExit {
|
|
66
|
+
code: number | null;
|
|
67
|
+
signal: string | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A spawned child, uniform across providers. `stdout`/`stderr` are the drains
|
|
71
|
+
* the caller (process registry) owns; `wait()` resolves once the child has
|
|
72
|
+
* exited *and* its pipes are closed, so buffered output is never lost.
|
|
73
|
+
*/
|
|
74
|
+
export interface SandboxChild {
|
|
75
|
+
readonly pid: number | null;
|
|
76
|
+
readonly stdin: Writable | null;
|
|
77
|
+
readonly stdout: Readable | null;
|
|
78
|
+
readonly stderr: Readable | null;
|
|
79
|
+
wait(): Promise<SandboxExit>;
|
|
80
|
+
/** SIGTERM the process tree (best effort). */
|
|
81
|
+
terminate(): void;
|
|
82
|
+
/** SIGKILL the process tree (best effort). */
|
|
83
|
+
kill(): void;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* F4: per-call address-space exemption.
|
|
87
|
+
*
|
|
88
|
+
* Modern Chromium reserves an enormous VIRTUAL address space; under a 2GiB
|
|
89
|
+
* `RLIMIT_AS` it dies with SIGTRAP before it can log anything (measured:
|
|
90
|
+
* 2/3/4/8/16/32GiB all die, 64GiB lives). Setting this on ONE call omits
|
|
91
|
+
* `RLIMIT_AS` and NOTHING ELSE — CPU/NPROC/FSIZE/NOFILE/CORE still apply.
|
|
92
|
+
*
|
|
93
|
+
* Absent/undefined = the historical behaviour (address space limited).
|
|
94
|
+
* Deliberately NOT surfaced in `SandboxMeta`: the model-visible contract stays
|
|
95
|
+
* the four fields it was just reduced to; the tool layer states the exemption
|
|
96
|
+
* in its own result.
|
|
97
|
+
*/
|
|
98
|
+
export interface SandboxAddressSpaceOptions {
|
|
99
|
+
noAddressSpaceLimit?: boolean;
|
|
100
|
+
}
|
|
101
|
+
export interface SandboxRunRequest extends SandboxAddressSpaceOptions {
|
|
102
|
+
command: string;
|
|
103
|
+
/** Optional per-call cwd; must exist inside `config.root`. */
|
|
104
|
+
workdir?: string;
|
|
105
|
+
/** Optional per-call kill deadline, bounded by `config.maxTimeoutMs`. */
|
|
106
|
+
timeoutMs?: number;
|
|
107
|
+
/** W6: optional per-call `RLIMIT_CPU` in seconds, clamped to `config.maxCpuSec`. */
|
|
108
|
+
cpuSec?: number;
|
|
109
|
+
}
|
|
110
|
+
export interface SandboxSpawnRequest extends SandboxAddressSpaceOptions {
|
|
111
|
+
command: string;
|
|
112
|
+
workdir?: string;
|
|
113
|
+
/** W6: optional per-call `RLIMIT_CPU` in seconds, clamped to `config.maxCpuSec`. */
|
|
114
|
+
cpuSec?: number;
|
|
115
|
+
}
|
|
116
|
+
/** Result of a foreground run: capped streams + exit code + effective mode. */
|
|
117
|
+
export interface SandboxRunResult {
|
|
118
|
+
stdout: string;
|
|
119
|
+
stderr: string;
|
|
120
|
+
exit_code: number | null;
|
|
121
|
+
/** W6: terminating signal when the child was killed (SIGXCPU/SIGKILL on a CPU cap). */
|
|
122
|
+
signal?: string | null;
|
|
123
|
+
stdout_truncated: boolean;
|
|
124
|
+
stderr_truncated: boolean;
|
|
125
|
+
sandbox: SandboxMeta;
|
|
126
|
+
}
|
|
127
|
+
/** A detached background child plus the mode it was spawned under. */
|
|
128
|
+
export interface SandboxSpawned {
|
|
129
|
+
child: SandboxChild;
|
|
130
|
+
sandbox: SandboxMeta;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* W885: the inputs a `run_code` broker needs to quote its interpreter line for
|
|
134
|
+
* the shell that will actually carry it. Structural on purpose — `core` is the
|
|
135
|
+
* dependency-free leaf and cannot import the tools implementation — and
|
|
136
|
+
* OPTIONAL: an absent field means the host default (the `celestea-home.ts`
|
|
137
|
+
* defaulting rule).
|
|
138
|
+
*/
|
|
139
|
+
export interface SandboxShellLookup {
|
|
140
|
+
/** `process.platform` equivalent. */
|
|
141
|
+
platform?: NodeJS.Platform | string;
|
|
142
|
+
/** `process.env` equivalent. */
|
|
143
|
+
env?: Record<string, string | undefined>;
|
|
144
|
+
/** PATH lookup seam (bare executable name -> absolute path or null). */
|
|
145
|
+
which?: (bin: string) => string | null;
|
|
146
|
+
/** Existence check for absolute candidate paths. */
|
|
147
|
+
exists?: (path: string) => boolean;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The execution boundary. `run` is the foreground path (deadline enforced,
|
|
151
|
+
* stdio captured); `spawn` is the background path (no deadline, stdin piped so
|
|
152
|
+
* `process_control` can write lines).
|
|
153
|
+
*/
|
|
154
|
+
export interface Sandbox {
|
|
155
|
+
readonly config: SandboxConfig;
|
|
156
|
+
/**
|
|
157
|
+
* W885: the platform/shell view this provider runs commands under. Absent =
|
|
158
|
+
* host defaults; tests inject a win32 view to exercise the Windows branch.
|
|
159
|
+
*/
|
|
160
|
+
readonly shell?: SandboxShellLookup;
|
|
161
|
+
run(req: SandboxRunRequest): Promise<SandboxRunResult>;
|
|
162
|
+
spawn(req: SandboxSpawnRequest): Promise<SandboxSpawned>;
|
|
163
|
+
}
|
|
164
|
+
/** Stable error kinds (mirrors the engine's `SandboxError::code`). */
|
|
165
|
+
export type SandboxErrorKind = "timeout" | "workdir" | "arg" | "config" | "spawn";
|
|
166
|
+
/** Stable prefix of every structured sandbox error (contract, not decoration). */
|
|
167
|
+
export declare const SANDBOX_ERROR_PREFIX = "run_shell-sandbox";
|
|
168
|
+
/** Escape + truncate a message so the one-line error contract stays parseable. */
|
|
169
|
+
export declare function quoteSandboxMessage(message: string): string;
|
|
170
|
+
/**
|
|
171
|
+
* Structured sandbox failure. `message` is the contract string
|
|
172
|
+
* `run_shell-sandbox: code=<kind> msg="<quoted>"`; `detail` carries the
|
|
173
|
+
* machine-readable extras (pid, captured byte counts, requested workdir, …).
|
|
174
|
+
*/
|
|
175
|
+
export declare class SandboxError extends Error {
|
|
176
|
+
readonly kind: SandboxErrorKind;
|
|
177
|
+
readonly detail: Readonly<Record<string, unknown>>;
|
|
178
|
+
constructor(kind: SandboxErrorKind, message: string, detail?: Record<string, unknown>);
|
|
179
|
+
}
|
|
180
|
+
export declare function isSandboxError(value: unknown): value is SandboxError;
|
|
181
|
+
/** Well-known token for the sandbox service in a Context. */
|
|
182
|
+
export declare const SANDBOX_SERVICE = "celestea.core.Sandbox";
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox seam — the injected execution boundary behind `run_shell`.
|
|
3
|
+
*
|
|
4
|
+
* Parity target: `crates/tools/src/sandbox.rs` (`SandboxConfig`, `SandboxMeta`,
|
|
5
|
+
* `SandboxOutput`, `SandboxError`, `execute_sandboxed`, `spawn_sandboxed`) plus
|
|
6
|
+
* the `OsSandboxLayer` hook that lets an OS-level layer (bubblewrap / raw
|
|
7
|
+
* namespaces / seccomp) wrap the direct command without touching call sites.
|
|
8
|
+
*
|
|
9
|
+
* Why a seam in `core`: `run_shell` must be pure orchestration (argument
|
|
10
|
+
* handling, timeout/cap bookkeeping, background hand-off) and must never know
|
|
11
|
+
* *how* isolation is achieved. Implementations are plugins: P2b ships the
|
|
12
|
+
* userspace implementation in `@celestea/tools`; the real OS isolation lands in
|
|
13
|
+
* P2c behind this exact interface (ARCHITECTURE.md §3.1, §7.4).
|
|
14
|
+
*
|
|
15
|
+
* Invariants kept from the legacy engine:
|
|
16
|
+
* - the effective isolation mode travels *inside* every result (`SandboxMeta`):
|
|
17
|
+
* a caller never infers the isolation level, and silent degradation stays
|
|
18
|
+
* visible;
|
|
19
|
+
* - every failure is a structured `SandboxError` — `run_shell-sandbox: code=<k>
|
|
20
|
+
* msg="<quoted>"` — never a bare string, never a thrown non-Error;
|
|
21
|
+
* - a kill deadline is enforced for foreground runs; background runs carry no
|
|
22
|
+
* call-level deadline (they outlive the turn) and are reaped by the caller.
|
|
23
|
+
*/
|
|
24
|
+
/** The userspace (no OS isolation) mode: everything reported, nothing hidden. */
|
|
25
|
+
export const USERSPACE_SANDBOX_META = {
|
|
26
|
+
provider: "userspace",
|
|
27
|
+
net_isolated: false,
|
|
28
|
+
tmp_private: false,
|
|
29
|
+
seccomp: false,
|
|
30
|
+
};
|
|
31
|
+
/** Stable prefix of every structured sandbox error (contract, not decoration). */
|
|
32
|
+
export const SANDBOX_ERROR_PREFIX = "run_shell-sandbox";
|
|
33
|
+
/** Escape + truncate a message so the one-line error contract stays parseable. */
|
|
34
|
+
export function quoteSandboxMessage(message) {
|
|
35
|
+
let out = "";
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const ch of message) {
|
|
38
|
+
if (count >= 512)
|
|
39
|
+
break;
|
|
40
|
+
count += 1;
|
|
41
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
42
|
+
if (ch === "\\")
|
|
43
|
+
out += "\\\\";
|
|
44
|
+
else if (ch === '"')
|
|
45
|
+
out += '\\"';
|
|
46
|
+
else if (ch === "\n")
|
|
47
|
+
out += "\\n";
|
|
48
|
+
else if (ch === "\r")
|
|
49
|
+
out += "\\r";
|
|
50
|
+
else if (ch === "\t")
|
|
51
|
+
out += "\\t";
|
|
52
|
+
else if (cp < 0x20)
|
|
53
|
+
out += `\\u{${cp.toString(16)}}`;
|
|
54
|
+
else
|
|
55
|
+
out += ch;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Structured sandbox failure. `message` is the contract string
|
|
61
|
+
* `run_shell-sandbox: code=<kind> msg="<quoted>"`; `detail` carries the
|
|
62
|
+
* machine-readable extras (pid, captured byte counts, requested workdir, …).
|
|
63
|
+
*/
|
|
64
|
+
export class SandboxError extends Error {
|
|
65
|
+
kind;
|
|
66
|
+
detail;
|
|
67
|
+
constructor(kind, message, detail = {}) {
|
|
68
|
+
super(`${SANDBOX_ERROR_PREFIX}: code=${kind} msg="${quoteSandboxMessage(message)}"`);
|
|
69
|
+
this.name = "SandboxError";
|
|
70
|
+
this.kind = kind;
|
|
71
|
+
this.detail = detail;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function isSandboxError(value) {
|
|
75
|
+
return value instanceof SandboxError;
|
|
76
|
+
}
|
|
77
|
+
/** Well-known token for the sandbox service in a Context. */
|
|
78
|
+
export const SANDBOX_SERVICE = "celestea.core.Sandbox";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionEvent codec — the serde-exact JSONL row contract of
|
|
3
|
+
* `crates/core/src/session_log.rs:44-85`.
|
|
4
|
+
*
|
|
5
|
+
* Wire rules (all of them are contract):
|
|
6
|
+
* - internally tagged enum: the `type` key comes FIRST, then the fields in
|
|
7
|
+
* declaration order (`id`, `outcome` / `id`, `name`, `args`, `parent_id`);
|
|
8
|
+
* - `parent_id` is `Option<String>` with `skip_serializing_if = "Option::is_none"`,
|
|
9
|
+
* so it is omitted when absent (pre-W255 byte shape) — but a JSON `null`
|
|
10
|
+
* also deserializes to `None`;
|
|
11
|
+
* - `value` / `error` are `Option<…>` without skip: they are ALWAYS written
|
|
12
|
+
* and become `null` when absent;
|
|
13
|
+
* - `TurnEnd.outcome` has `#[serde(default)]`: a legacy row without it reads
|
|
14
|
+
* as `"completed"` and is re-written WITH the field;
|
|
15
|
+
* - unknown fields are ignored; a missing required field or an unknown
|
|
16
|
+
* `type` is a parse error (the caller then treats the row as a torn tail);
|
|
17
|
+
* - `args` / `value` ride through `serde_json::Value`, so their object keys
|
|
18
|
+
* are re-serialized in sorted order (see `serdeJsonString`).
|
|
19
|
+
*/
|
|
20
|
+
import { type SessionEvent, type SessionEventType, type TurnOutcome } from "./types.js";
|
|
21
|
+
export type ValidateResult = {
|
|
22
|
+
ok: true;
|
|
23
|
+
event: SessionEvent;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
errors: string[];
|
|
27
|
+
};
|
|
28
|
+
/** `TurnOutcome::default()` — legacy `turn_end` rows read as completed. */
|
|
29
|
+
export declare const DEFAULT_TURN_OUTCOME: TurnOutcome;
|
|
30
|
+
/** The outcome of a row, with the legacy default applied. */
|
|
31
|
+
export declare function effectiveOutcome(o: TurnOutcome | undefined): TurnOutcome;
|
|
32
|
+
/** Normalized outcome label (TurnOutcome -> the 5 SSE/statusline phases). */
|
|
33
|
+
export declare function outcomePhase(o: TurnOutcome | undefined): string;
|
|
34
|
+
/** `"{kind}: {message}"` for the error variant, null otherwise. */
|
|
35
|
+
export declare function outcomeError(o: TurnOutcome | undefined): string | null;
|
|
36
|
+
/** The error payload of the error variant, null otherwise. */
|
|
37
|
+
export declare function outcomeErrorParts(o: TurnOutcome | undefined): {
|
|
38
|
+
kind: string;
|
|
39
|
+
message: string;
|
|
40
|
+
} | null;
|
|
41
|
+
export declare function isTurnOutcome(v: unknown): v is TurnOutcome;
|
|
42
|
+
export declare function isSessionEventType(t: string): t is SessionEventType;
|
|
43
|
+
/** Validate one decoded JSON value against the SessionEvent contract. */
|
|
44
|
+
export declare function validateSessionEvent(raw: unknown): ValidateResult;
|
|
45
|
+
/** Parse one JSONL row; a failure is the caller's torn-tail signal. */
|
|
46
|
+
export declare function parseSessionEvent(line: string): ValidateResult;
|
|
47
|
+
/**
|
|
48
|
+
* Serialize one event exactly like `serde_json::to_string(&SessionEvent)`:
|
|
49
|
+
* the `type` tag first, then the fields in declaration order, `parent_id`
|
|
50
|
+
* omitted when None, `value` / `error` always present (null when None), and
|
|
51
|
+
* `outcome` always present (legacy rows are normalised to `"completed"`).
|
|
52
|
+
*
|
|
53
|
+
* The event struct order is preserved (serde writes struct fields in
|
|
54
|
+
* declaration order); only the `Value`-typed fields (`args`, `value`) go
|
|
55
|
+
* through the sorted-key `serdeJsonString`, exactly like serde_json's BTreeMap.
|
|
56
|
+
*/
|
|
57
|
+
export declare function serializeSessionEvent(ev: SessionEvent): string;
|