@steerable/agent-runtime 0.0.0-stage → 0.9.0

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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The sidecar JSON-RPC method surface this runtime covers, as data.
3
+ *
4
+ * This list is the TS side of the 3.2.4 conformance gate:
5
+ * `test/surface.test.ts` parses the `register("<method>", …)` lines out of
6
+ * the Python sidecar (`packages/sidecar/py/src/steerable_sidecar/sidecar.py`)
7
+ * and asserts the two sets are identical — so a method added to the sidecar
8
+ * without a runtime wrapper (or a wrapper for a method the sidecar no longer
9
+ * has) fails CI instead of drifting silently.
10
+ */
11
+ /** Every host→sidecar request method the sidecar registers. */
12
+ export const SIDECAR_METHODS = [
13
+ 'system.ping',
14
+ 'system.shutdown',
15
+ 'system.shutdown_now',
16
+ 'agent.session.create',
17
+ 'agent.session.resume',
18
+ 'agent.session.list',
19
+ 'agent.session.fork',
20
+ 'agent.session.branches',
21
+ 'agent.session.messages',
22
+ 'agent.chat.stream',
23
+ 'agent.chat.cancel',
24
+ 'agent.chat.steer',
25
+ 'agent.chat.fork',
26
+ 'agent.chat.compact',
27
+ 'agent.session.tree',
28
+ 'tool.list',
29
+ 'tool.invoke',
30
+ 'workspace.apply_edits',
31
+ 'skills.list',
32
+ 'trace.fetch',
33
+ 'trace.export',
34
+ 'metrics.export',
35
+ 'config.get',
36
+ 'config.set',
37
+ 'compat.describe',
38
+ 'sandbox.describe',
39
+ 'harness.describe',
40
+ 'models.list',
41
+ 'presets.describe',
42
+ 'presets.resolve',
43
+ 'catalog.describe',
44
+ 'plugin.list',
45
+ 'plugin.enable',
46
+ 'plugin.disable',
47
+ 'plugin.reload',
48
+ 'plugin.tools.describe',
49
+ ];
50
+ /** Notification methods the sidecar pushes (no id, no response). */
51
+ export const SIDECAR_NOTIFICATIONS = [
52
+ 'lifecycle.ready',
53
+ 'lifecycle.shutdown',
54
+ 'stream.chunk',
55
+ 'stream.done',
56
+ 'stream.error',
57
+ 'agent.child',
58
+ ];
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Locates the platform runtime bundle installed by `@steerable/runtime-<target>`.
3
+ *
4
+ * A global npm install of the CLI has no packaged `python-runtime/` directory
5
+ * and no framework checkout, so the last fallback before failing is the
6
+ * per-platform package npm picked for this host. The package carries a
7
+ * `runtime.json` descriptor with relative paths and SHA-256 digests; the
8
+ * sidecar interpreter and egress proxy are hashed on first use, the whole
9
+ * declared set only under `doctor --deep`.
10
+ *
11
+ * Lives in Tier 1 because both the host shell (bundled resources, egress
12
+ * proxy) and the published runtime (`@steerable/agent-runtime`) resolve the
13
+ * same bundle, and neither may depend on the host shell.
14
+ */
15
+ import { type RuntimeTarget } from '@steerable/agent-protocol';
16
+ /** One file the platform package declares, relative to the package root. */
17
+ export interface RuntimeFile {
18
+ path: string;
19
+ sha256: string;
20
+ }
21
+ /** Parsed `runtime.json` of one platform package. */
22
+ export interface PlatformRuntimeManifest {
23
+ schemaVersion: 1;
24
+ target: RuntimeTarget;
25
+ /** Rust engine version this bundle was built against. */
26
+ engineVersion: string;
27
+ /** Package version that shipped the bundle. */
28
+ packageVersion: string;
29
+ sidecarPython: string;
30
+ runCodePython: string;
31
+ egressProxy: string | null;
32
+ winSpawnHelper: string | null;
33
+ files: RuntimeFile[];
34
+ }
35
+ /** A resolved, digest-checked platform runtime. */
36
+ export interface PlatformRuntime {
37
+ manifest: PlatformRuntimeManifest;
38
+ packageName: string;
39
+ packageDir: string;
40
+ sidecarPython: string;
41
+ runCodePython: string;
42
+ egressProxy: string | null;
43
+ winSpawnHelper: string | null;
44
+ }
45
+ /** Why no platform runtime was resolved. */
46
+ export type PlatformRuntimeFailureReason = 'unsupported_platform' | 'not_installed' | 'invalid_manifest' | 'digest_mismatch';
47
+ export interface PlatformRuntimeFailure {
48
+ kind: 'error';
49
+ reason: PlatformRuntimeFailureReason;
50
+ /** Host target, or null when the platform itself is unpublished. */
51
+ target: RuntimeTarget | null;
52
+ detail: string;
53
+ /** File that failed the digest check, when `reason` is `digest_mismatch`. */
54
+ file?: string;
55
+ expected?: string;
56
+ actual?: string;
57
+ }
58
+ export type PlatformRuntimeResult = {
59
+ kind: 'ok';
60
+ runtime: PlatformRuntime;
61
+ } | PlatformRuntimeFailure;
62
+ /** Default package scope for platform bundles. */
63
+ export declare const PLATFORM_PACKAGE_SCOPE = "@steerable/runtime-";
64
+ /** Drops the per-process cache; tests and `doctor --deep` use it. */
65
+ export declare function resetPlatformRuntimeCache(): void;
66
+ /** @param target A published runtime target. @returns The npm package name that ships it. */
67
+ export declare function platformPackageName(target: RuntimeTarget): string;
68
+ /**
69
+ * Resolves the platform runtime for this host, checking the two executables
70
+ * the sidecar boot path needs.
71
+ *
72
+ * The result is cached per process: a global install cannot gain a different
73
+ * bundle while the process runs, and hashing the interpreter on every tool
74
+ * call would add startup cost for no benefit.
75
+ *
76
+ * @returns The resolved runtime, or a structured failure that names the fix.
77
+ */
78
+ export declare function resolvePlatformRuntime(): PlatformRuntimeResult;
79
+ /**
80
+ * Verifies every file the manifest declares, not just the executables the
81
+ * boot path touches. `doctor --deep` reports the first mismatch.
82
+ *
83
+ * @returns The mismatch, or null when every declared file matches.
84
+ */
85
+ export declare function verifyPlatformRuntimeDeep(runtime: PlatformRuntime): PlatformRuntimeFailure | null;
86
+ /** @returns A one-line description of the supported targets, for error text. */
87
+ export declare function supportedTargetsLine(): string;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Locates the platform runtime bundle installed by `@steerable/runtime-<target>`.
3
+ *
4
+ * A global npm install of the CLI has no packaged `python-runtime/` directory
5
+ * and no framework checkout, so the last fallback before failing is the
6
+ * per-platform package npm picked for this host. The package carries a
7
+ * `runtime.json` descriptor with relative paths and SHA-256 digests; the
8
+ * sidecar interpreter and egress proxy are hashed on first use, the whole
9
+ * declared set only under `doctor --deep`.
10
+ *
11
+ * Lives in Tier 1 because both the host shell (bundled resources, egress
12
+ * proxy) and the published runtime (`@steerable/agent-runtime`) resolve the
13
+ * same bundle, and neither may depend on the host shell.
14
+ */
15
+ import { createHash } from 'node:crypto';
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+ import { createRequire } from 'node:module';
18
+ import { dirname, isAbsolute, join, normalize } from 'node:path';
19
+ import { hostRuntimeTarget, RUNTIME_TARGETS } from '@steerable/agent-protocol';
20
+ /** Default package scope for platform bundles. */
21
+ export const PLATFORM_PACKAGE_SCOPE = '@steerable/runtime-';
22
+ let cached = null;
23
+ /** Drops the per-process cache; tests and `doctor --deep` use it. */
24
+ export function resetPlatformRuntimeCache() {
25
+ cached = null;
26
+ }
27
+ /** @param target A published runtime target. @returns The npm package name that ships it. */
28
+ export function platformPackageName(target) {
29
+ return `${PLATFORM_PACKAGE_SCOPE}${target}`;
30
+ }
31
+ /**
32
+ * Resolves the platform runtime for this host, checking the two executables
33
+ * the sidecar boot path needs.
34
+ *
35
+ * The result is cached per process: a global install cannot gain a different
36
+ * bundle while the process runs, and hashing the interpreter on every tool
37
+ * call would add startup cost for no benefit.
38
+ *
39
+ * @returns The resolved runtime, or a structured failure that names the fix.
40
+ */
41
+ export function resolvePlatformRuntime() {
42
+ if (cached)
43
+ return cached;
44
+ cached = resolveUncached();
45
+ return cached;
46
+ }
47
+ function resolveUncached() {
48
+ const target = hostRuntimeTarget(process.platform, process.arch);
49
+ if (!target) {
50
+ return {
51
+ kind: 'error',
52
+ reason: 'unsupported_platform',
53
+ target: null,
54
+ detail: `${process.platform}-${process.arch}`,
55
+ };
56
+ }
57
+ const packageName = platformPackageName(target);
58
+ const require = createRequire(import.meta.url);
59
+ let manifestPath;
60
+ try {
61
+ manifestPath = require.resolve(`${packageName}/runtime.json`);
62
+ }
63
+ catch {
64
+ // The package is absent, or present without the descriptor: both mean the
65
+ // install cannot supply a verified runtime.
66
+ return {
67
+ kind: 'error',
68
+ reason: 'not_installed',
69
+ target,
70
+ detail: packageName,
71
+ };
72
+ }
73
+ const packageDir = dirname(manifestPath);
74
+ const manifest = readManifest(manifestPath, target, packageName);
75
+ if (typeof manifest === 'string') {
76
+ return { kind: 'error', reason: 'invalid_manifest', target, detail: manifest };
77
+ }
78
+ const sidecarPython = resolveDeclaredFile(packageDir, manifest.sidecarPython);
79
+ const runCodePython = resolveDeclaredFile(packageDir, manifest.runCodePython);
80
+ if (!sidecarPython || !runCodePython) {
81
+ return {
82
+ kind: 'error',
83
+ reason: 'invalid_manifest',
84
+ target,
85
+ detail: `${packageName}: declared interpreter is missing from the package`,
86
+ };
87
+ }
88
+ const sidecarDigest = manifest.files.find((file) => file.path === manifest.sidecarPython);
89
+ const mismatch = sidecarDigest ? verifyFile(packageDir, sidecarDigest) : null;
90
+ if (mismatch)
91
+ return { ...mismatch, target };
92
+ const proxy = manifest.egressProxy ? resolveDeclaredFile(packageDir, manifest.egressProxy) : null;
93
+ const proxyDigest = manifest.files.find((file) => file.path === manifest.egressProxy);
94
+ if (proxy && proxyDigest) {
95
+ const proxyMismatch = verifyFile(packageDir, proxyDigest);
96
+ if (proxyMismatch)
97
+ return { ...proxyMismatch, target };
98
+ }
99
+ return {
100
+ kind: 'ok',
101
+ runtime: {
102
+ manifest,
103
+ packageName,
104
+ packageDir,
105
+ sidecarPython,
106
+ runCodePython,
107
+ egressProxy: proxy,
108
+ winSpawnHelper: manifest.winSpawnHelper
109
+ ? resolveDeclaredFile(packageDir, manifest.winSpawnHelper)
110
+ : null,
111
+ },
112
+ };
113
+ }
114
+ /**
115
+ * Verifies every file the manifest declares, not just the executables the
116
+ * boot path touches. `doctor --deep` reports the first mismatch.
117
+ *
118
+ * @returns The mismatch, or null when every declared file matches.
119
+ */
120
+ export function verifyPlatformRuntimeDeep(runtime) {
121
+ for (const file of runtime.manifest.files) {
122
+ const mismatch = verifyFile(runtime.packageDir, file);
123
+ if (mismatch)
124
+ return { ...mismatch, target: runtime.manifest.target };
125
+ }
126
+ return null;
127
+ }
128
+ function readManifest(manifestPath, target, packageName) {
129
+ let raw;
130
+ try {
131
+ raw = JSON.parse(readFileSync(manifestPath, 'utf8'));
132
+ }
133
+ catch (error) {
134
+ return `${packageName}: unreadable runtime.json (${error instanceof Error ? error.message : String(error)})`;
135
+ }
136
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
137
+ return `${packageName}: runtime.json is not an object`;
138
+ const record = raw;
139
+ if (record.schemaVersion !== 1)
140
+ return `${packageName}: unknown runtime.json schemaVersion`;
141
+ if (record.target !== target)
142
+ return `${packageName}: runtime.json targets ${String(record.target)}`;
143
+ if (typeof record.sidecarPython !== 'string' || !record.sidecarPython) {
144
+ return `${packageName}: runtime.json has no sidecarPython`;
145
+ }
146
+ if (typeof record.runCodePython !== 'string' || !record.runCodePython) {
147
+ return `${packageName}: runtime.json has no runCodePython`;
148
+ }
149
+ const files = [];
150
+ if (Array.isArray(record.files)) {
151
+ for (const entry of record.files) {
152
+ if (!entry || typeof entry !== 'object')
153
+ continue;
154
+ const row = entry;
155
+ if (typeof row.path === 'string' && typeof row.sha256 === 'string') {
156
+ files.push({ path: row.path, sha256: row.sha256.toLowerCase() });
157
+ }
158
+ }
159
+ }
160
+ if (files.length === 0)
161
+ return `${packageName}: runtime.json declares no files`;
162
+ return {
163
+ schemaVersion: 1,
164
+ target,
165
+ engineVersion: typeof record.engineVersion === 'string' ? record.engineVersion : '',
166
+ packageVersion: typeof record.packageVersion === 'string' ? record.packageVersion : '',
167
+ sidecarPython: record.sidecarPython,
168
+ runCodePython: record.runCodePython,
169
+ egressProxy: typeof record.egressProxy === 'string' && record.egressProxy ? record.egressProxy : null,
170
+ winSpawnHelper: typeof record.winSpawnHelper === 'string' && record.winSpawnHelper ? record.winSpawnHelper : null,
171
+ files,
172
+ };
173
+ }
174
+ /** Declared paths are package-relative; anything absolute or escaping the package is refused. */
175
+ function resolveDeclaredFile(packageDir, relative) {
176
+ if (isAbsolute(relative))
177
+ return null;
178
+ const resolved = normalize(join(packageDir, relative));
179
+ const root = normalize(packageDir);
180
+ if (resolved !== root && !resolved.startsWith(`${root}/`))
181
+ return null;
182
+ return existsSync(resolved) ? resolved : null;
183
+ }
184
+ function verifyFile(packageDir, file) {
185
+ const path = resolveDeclaredFile(packageDir, file.path);
186
+ if (!path) {
187
+ return {
188
+ kind: 'error',
189
+ reason: 'digest_mismatch',
190
+ detail: `declared file is missing: ${file.path}`,
191
+ file: file.path,
192
+ expected: file.sha256,
193
+ actual: '(missing)',
194
+ };
195
+ }
196
+ const actual = createHash('sha256').update(readFileSync(path)).digest('hex');
197
+ if (actual === file.sha256)
198
+ return null;
199
+ return {
200
+ kind: 'error',
201
+ reason: 'digest_mismatch',
202
+ detail: file.path,
203
+ file: file.path,
204
+ expected: file.sha256,
205
+ actual,
206
+ };
207
+ }
208
+ /** @returns A one-line description of the supported targets, for error text. */
209
+ export function supportedTargetsLine() {
210
+ return RUNTIME_TARGETS.join(', ');
211
+ }