@the-open-engine/zeroshot 6.24.0 → 6.25.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/cli/index.js +135 -72
- package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/omp.js +37 -11
- package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-driver.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-driver.js +27 -2
- package/lib/agent-cli-provider/omp-rpc-driver.js.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-session.js +3 -3
- package/lib/agent-cli-provider/omp-rpc-session.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +7 -1
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +2 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +66 -2
- package/src/agent/agent-task-executor.js +72 -3
- package/src/agent/provider-session.js +125 -2
- package/src/agent-cli-provider/adapters/omp.ts +41 -11
- package/src/agent-cli-provider/omp-rpc-driver.ts +31 -3
- package/src/agent-cli-provider/omp-rpc-session.ts +3 -3
- package/src/agent-cli-provider/provider-registry.ts +7 -1
- package/src/agent-cli-provider/types.ts +4 -0
- package/src/omp-blob-root.js +110 -0
- package/src/omp-config-overlay.js +9 -1
- package/src/omp-execution-fingerprint.js +62 -0
- package/src/omp-session-limits.js +41 -0
- package/src/omp-session-partition.js +424 -0
- package/src/omp-session-verifier.js +740 -0
- package/task-lib/commands/clean.js +92 -35
- package/task-lib/commands/kill.js +21 -0
- package/task-lib/commands/resume.js +62 -0
- package/task-lib/commands/run.js +80 -0
- package/task-lib/omp-session-cleanup.js +197 -0
- package/task-lib/omp-session-ownership-schema.js +268 -0
- package/task-lib/omp-session-ownership.js +367 -0
- package/task-lib/omp-storage-root.js +35 -0
- package/task-lib/rpc-watcher.js +368 -2
- package/task-lib/runner.js +243 -5
- package/task-lib/store.js +106 -76
|
@@ -4,6 +4,7 @@ import { appendJsonSchemaPrompt } from '../schema';
|
|
|
4
4
|
import { isRecord, unknownToMessage } from '../json';
|
|
5
5
|
import { OMP_REMEDIATION, OMP_SUPPORTED_VERSION } from '../omp-release';
|
|
6
6
|
import { parseNormalizedOmpRpcEventLine } from '../omp-rpc-events';
|
|
7
|
+
import type { OmpSessionLaunch } from '../omp-rpc-session';
|
|
7
8
|
import {
|
|
8
9
|
InvalidProviderModelError,
|
|
9
10
|
type BuildProviderCommandOptions,
|
|
@@ -180,8 +181,13 @@ function detectCliFeatures(helpText?: string | null, versionText?: string | null
|
|
|
180
181
|
};
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
function resolveOmpSessionLaunch(options: BuildProviderCommandOptions): OmpSessionLaunch {
|
|
185
|
+
return options.ompSession ?? { kind: 'none' };
|
|
186
|
+
}
|
|
187
|
+
|
|
183
188
|
function assertRequiredOmpFeatures(options: BuildProviderCommandOptions): void {
|
|
184
189
|
const features = optionFeatures(options);
|
|
190
|
+
const session = resolveOmpSessionLaunch(options);
|
|
185
191
|
const required: ReadonlyArray<readonly [boolean | undefined, string]> = [
|
|
186
192
|
[features.versionMatches, `exact OMP version ${OMP_SUPPORTED_VERSION}`],
|
|
187
193
|
[features.supportsRpcMode, '"rpc" mode'],
|
|
@@ -190,6 +196,10 @@ function assertRequiredOmpFeatures(options: BuildProviderCommandOptions): void {
|
|
|
190
196
|
[features.supportsApprovalMode, '--approval-mode'],
|
|
191
197
|
[features.supportsNoTitle, '--no-title'],
|
|
192
198
|
[features.supportsNoSession, '--no-session'],
|
|
199
|
+
...(session.kind === 'none'
|
|
200
|
+
? []
|
|
201
|
+
: ([[features.supportsSessionDir, '--session-dir']] as const)),
|
|
202
|
+
...(session.kind === 'resume' ? ([[features.supportsResume, '--resume']] as const) : []),
|
|
193
203
|
];
|
|
194
204
|
const missing = required.filter(([supported]) => supported === false).map(([, label]) => label);
|
|
195
205
|
if (missing.length === 0) return;
|
|
@@ -203,16 +213,35 @@ function assertRequiredOmpFeatures(options: BuildProviderCommandOptions): void {
|
|
|
203
213
|
}
|
|
204
214
|
|
|
205
215
|
function failClosedUnsupportedSessionControl(options: BuildProviderCommandOptions): void {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
if (options.continueSession) {
|
|
217
|
+
throw contractError({
|
|
218
|
+
code: 'invalid-field',
|
|
219
|
+
field: 'options.continueSession',
|
|
220
|
+
exitCode: 2,
|
|
221
|
+
message: 'OMP RPC lane never supports --continue; continuation is always an explicit verified --resume partition.',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const hasVerifiedResume = resolveOmpSessionLaunch(options).kind === 'resume';
|
|
225
|
+
if (options.resumeSessionId !== undefined && !hasVerifiedResume) {
|
|
226
|
+
throw contractError({
|
|
227
|
+
code: 'invalid-field',
|
|
228
|
+
field: 'options.resumeSessionId',
|
|
229
|
+
exitCode: 2,
|
|
230
|
+
message:
|
|
231
|
+
'OMP RPC lane requires a verified session partition (options.ompSession.kind === "resume") to resume; a bare session ID cannot be trusted.',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function sessionArgs(session: OmpSessionLaunch): readonly string[] {
|
|
237
|
+
switch (session.kind) {
|
|
238
|
+
case 'none':
|
|
239
|
+
return ['--no-session'];
|
|
240
|
+
case 'fresh':
|
|
241
|
+
return ['--session-dir', session.partition.path];
|
|
242
|
+
case 'resume':
|
|
243
|
+
return ['--session-dir', session.partition.path, '--resume', session.file.path];
|
|
244
|
+
}
|
|
216
245
|
}
|
|
217
246
|
|
|
218
247
|
function rejectMcpConfig(options: BuildProviderCommandOptions): void {
|
|
@@ -257,8 +286,9 @@ function buildCommand(_context: string, options: BuildProviderCommandOptions = {
|
|
|
257
286
|
const modelSelector = resolveModelSelector(options);
|
|
258
287
|
const warnings = collectWarnings(options);
|
|
259
288
|
const overlay = createOmpConfigOverlay();
|
|
289
|
+
const session = resolveOmpSessionLaunch(options);
|
|
260
290
|
|
|
261
|
-
const args: string[] = ['--mode', 'rpc',
|
|
291
|
+
const args: string[] = ['--mode', 'rpc', ...sessionArgs(session), '--model', modelSelector];
|
|
262
292
|
if (options.modelSpec?.reasoningEffort) {
|
|
263
293
|
args.push('--thinking', options.modelSpec.reasoningEffort);
|
|
264
294
|
}
|
|
@@ -464,6 +464,20 @@ export function runOmpRpcTask(
|
|
|
464
464
|
// (agent_end / a delayed prompt_result), not by this function returning.
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
+
// Present on both the get_state response's `data` and a `session_info_update` event frame per
|
|
468
|
+
// docs/rpc.md; either may carry only a subset, so callers merge this onto prior evidence
|
|
469
|
+
// rather than replacing it wholesale.
|
|
470
|
+
function sessionFieldsFromRecord(
|
|
471
|
+
record: Record<string, unknown>
|
|
472
|
+
): Partial<Pick<OmpRpcSessionEvidence, 'sessionId' | 'sessionFile'>> {
|
|
473
|
+
const sessionId = getString(record, 'sessionId');
|
|
474
|
+
const sessionFile = getString(record, 'sessionFile');
|
|
475
|
+
return {
|
|
476
|
+
...(sessionId !== null ? { sessionId } : {}),
|
|
477
|
+
...(sessionFile !== null ? { sessionFile } : {}),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
467
481
|
function sessionEvidenceFromState(
|
|
468
482
|
stateResponse: Record<string, unknown> | null
|
|
469
483
|
): Omit<OmpRpcSessionEvidence, 'phase'> {
|
|
@@ -471,15 +485,27 @@ export function runOmpRpcTask(
|
|
|
471
485
|
const data = getRecord(stateResponse, 'data');
|
|
472
486
|
const model = data ? getRecord(data, 'model') : null;
|
|
473
487
|
return {
|
|
474
|
-
|
|
475
|
-
|
|
488
|
+
...UNKNOWN_SESSION_EVIDENCE,
|
|
489
|
+
...(data ? sessionFieldsFromRecord(data) : {}),
|
|
476
490
|
selectedProvider: (model ? getString(model, 'provider') : null) ?? '',
|
|
477
491
|
selectedModel: (model ? getString(model, 'id') : null) ?? '',
|
|
478
492
|
thinkingLevel: (data ? getString(data, 'thinkingLevel') : null) ?? '',
|
|
479
493
|
};
|
|
480
494
|
}
|
|
481
495
|
|
|
482
|
-
|
|
496
|
+
// session_info_update is a builtin slash-command side channel (docs/rpc.md) that can carry a
|
|
497
|
+
// later-observed sessionId/sessionFile than the initial get_state snapshot. Returning the
|
|
498
|
+
// hooks.onSession() promise (rather than fire-and-forget) lets a persistence failure surface
|
|
499
|
+
// through the same enqueue()/state.chain .catch() -> failPermanently() path as every other
|
|
500
|
+
// dispatch failure, instead of being silently swallowed.
|
|
501
|
+
function handleSessionInfoUpdate(frame: OmpRpcInboundFrame): Promise<void> | void {
|
|
502
|
+
const updates = sessionFieldsFromRecord(frame);
|
|
503
|
+
if (Object.keys(updates).length === 0) return;
|
|
504
|
+
state.sessionEvidence = { ...state.sessionEvidence, ...updates };
|
|
505
|
+
return hooks.onSession({ ...state.sessionEvidence, phase: 'ready' });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function dispatchFrame(frame: OmpRpcInboundFrame): void | Promise<void> {
|
|
483
509
|
if (state.terminal) return; // Frames after the terminal frame are dropped.
|
|
484
510
|
if (!state.readyReceived) {
|
|
485
511
|
dispatchReadyFrame(frame);
|
|
@@ -513,6 +539,8 @@ export function runOmpRpcTask(
|
|
|
513
539
|
case 'agent_end':
|
|
514
540
|
handleAgentEnd();
|
|
515
541
|
return;
|
|
542
|
+
case 'session_info_update':
|
|
543
|
+
return handleSessionInfoUpdate(frame);
|
|
516
544
|
default:
|
|
517
545
|
if (state.promptSent) emitNormalized(frame);
|
|
518
546
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Session-launch types for the OMP RPC v2 driver.
|
|
2
|
-
// (`--no-session`)
|
|
3
|
-
//
|
|
1
|
+
// Session-launch types for the OMP RPC v2 driver. `none` keeps the Docker-only sessionless launch
|
|
2
|
+
// (`--no-session`); `fresh`/`resume` carry a verified partition (and, for resume, a verified
|
|
3
|
+
// session file) allocated and checked by the JS task-lib layer — never a raw, unverified path.
|
|
4
4
|
|
|
5
5
|
export interface VerifiedOmpPartition {
|
|
6
6
|
readonly path: string;
|
|
@@ -486,6 +486,12 @@ export const providerRegistry = [
|
|
|
486
486
|
// Written out explicitly rather than spread from STANDARD_CAPABILITIES, which defaults
|
|
487
487
|
// dockerIsolation to true; OMP's Docker path is env/broker-only and sessionless (see
|
|
488
488
|
// AGENTS.md OMP Docker section) rather than the standard credential-mount + resume shape.
|
|
489
|
+
// sessionResume is true as of issue #866: verified UUID partitions, two-phase file
|
|
490
|
+
// verification, and the owner-fenced ownership FSM (task-lib/omp-session-ownership.js) are
|
|
491
|
+
// live end to end for host, worktree, detached cluster-agent, and standalone manual resume.
|
|
492
|
+
// The two are independent: an isolated (Docker) OMP task allocates no session partition at all
|
|
493
|
+
// and launches `--no-session`, so `sessionResume: true` never implies a resumable container
|
|
494
|
+
// turn (task-lib/runner.js#resolveOmpSessionPlan, OMP_SESSIONLESS_ENV).
|
|
489
495
|
capabilities: {
|
|
490
496
|
dockerIsolation: true,
|
|
491
497
|
worktreeIsolation: true,
|
|
@@ -494,7 +500,7 @@ export const providerRegistry = [
|
|
|
494
500
|
streamJson: true,
|
|
495
501
|
thinkingMode: true,
|
|
496
502
|
reasoningEffort: true,
|
|
497
|
-
sessionResume:
|
|
503
|
+
sessionResume: true,
|
|
498
504
|
webSearch: false,
|
|
499
505
|
},
|
|
500
506
|
docs: {
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
StructuredOutputProviderRegistryEntry,
|
|
13
13
|
UnstructuredOutputProviderRegistryEntry,
|
|
14
14
|
} from './provider-registry';
|
|
15
|
+
import type { OmpSessionLaunch } from './omp-rpc-session';
|
|
15
16
|
|
|
16
17
|
export type ProviderId = (typeof providerIds)[number];
|
|
17
18
|
export type ProviderAlias = (typeof providerAliases)[number];
|
|
@@ -329,6 +330,9 @@ export interface BuildProviderCommandOptions {
|
|
|
329
330
|
readonly autoApprove?: boolean;
|
|
330
331
|
readonly resumeSessionId?: string;
|
|
331
332
|
readonly continueSession?: boolean;
|
|
333
|
+
// OMP-only: a verified session launch (none/fresh/resume) built from a checked partition —
|
|
334
|
+
// never a raw session id string. See src/agent-cli-provider/omp-rpc-session.ts.
|
|
335
|
+
readonly ompSession?: OmpSessionLaunch;
|
|
332
336
|
readonly webSearch?: boolean;
|
|
333
337
|
readonly claudeSettingsFile?: string;
|
|
334
338
|
readonly cliFeatures?: CliFeatureOverrides;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Resolution of OMP's *shared* content-addressed blob store root, mirrored from the tagged
|
|
2
|
+
// v17.2.1 source (`packages/utils/src/dirs.ts`: `getBlobsDir()` / `DirResolver`) rather than
|
|
3
|
+
// invented by Zeroshot.
|
|
4
|
+
//
|
|
5
|
+
// Why this exists: OMP externalizes large payloads (images, provider data URLs) out of the session
|
|
6
|
+
// JSONL into `<blobsDir>/<sha256-hex>` and leaves a nested `blob:sha256:<hex>` reference string
|
|
7
|
+
// inside the JSONL record (`packages/coding-agent/src/session/blob-store.ts`). The store is shared
|
|
8
|
+
// by every session on the machine and lives at `~/.omp/agent/blobs` by default — nowhere near
|
|
9
|
+
// Zeroshot's per-task session partition. A resumed partition whose referenced blobs are missing is
|
|
10
|
+
// an invalid continuation, so verification has to resolve them at this real root; and because the
|
|
11
|
+
// root is shared, Zeroshot cleanup must never delete anything under it.
|
|
12
|
+
//
|
|
13
|
+
// Resolution order, exactly as `DirResolver`'s constructor computes it:
|
|
14
|
+
// profile = normalize(OMP_PROFILE ?? PI_PROFILE) // OMP_PROFILE wins; '' selects default
|
|
15
|
+
// configRoot = ~/${PI_CONFIG_DIR || '.omp'}[/profiles/<profile>]
|
|
16
|
+
// defaultAgent = <configRoot>/agent
|
|
17
|
+
// agentDir = profile ? defaultAgent : (resolve(PI_CODING_AGENT_DIR) || defaultAgent)
|
|
18
|
+
// dataBase = (linux|darwin) && agentDir === defaultAgent && $XDG_DATA_HOME/omp[/profiles/<p>]
|
|
19
|
+
// exists ? that : agentDir // XDG flattens the agent/ prefix
|
|
20
|
+
// blobsDir = <dataBase>/blobs
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const APP_NAME = 'omp';
|
|
26
|
+
const CONFIG_DIR_NAME = '.omp';
|
|
27
|
+
// dirs.ts PROFILE_NAME_RE / WINDOWS_RESERVED_BASENAME_RE.
|
|
28
|
+
const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
29
|
+
const WINDOWS_RESERVED_BASENAME_PATTERN = /^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/iu;
|
|
30
|
+
|
|
31
|
+
/** dirs.ts normalizeProfileName, but total: an invalid name resolves to the default profile here
|
|
32
|
+
* instead of throwing. A resume against a profile OMP itself would reject cannot succeed anyway —
|
|
33
|
+
* the verifier will simply not find the referenced blobs and fail the continuation closed. */
|
|
34
|
+
function normalizeProfileName(profile) {
|
|
35
|
+
const normalized = typeof profile === 'string' ? profile.trim() : '';
|
|
36
|
+
if (!normalized || normalized === 'default') return undefined;
|
|
37
|
+
if (
|
|
38
|
+
normalized === '.' ||
|
|
39
|
+
normalized === '..' ||
|
|
40
|
+
normalized.endsWith('.') ||
|
|
41
|
+
!PROFILE_NAME_PATTERN.test(normalized) ||
|
|
42
|
+
WINDOWS_RESERVED_BASENAME_PATTERN.test(normalized)
|
|
43
|
+
) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function activeProfile(env) {
|
|
50
|
+
return normalizeProfileName(
|
|
51
|
+
env.OMP_PROFILE !== undefined ? env.OMP_PROFILE : env.PI_PROFILE
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function directoryExists(candidate) {
|
|
56
|
+
try {
|
|
57
|
+
return fs.statSync(candidate).isDirectory();
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Absolute path of the shared OMP blob store for the current environment.
|
|
65
|
+
* `env`/`homedir`/`platform` are injectable for tests only; production callers pass nothing.
|
|
66
|
+
*/
|
|
67
|
+
function resolveOmpBlobsDir({
|
|
68
|
+
env = process.env,
|
|
69
|
+
homedir = os.homedir(),
|
|
70
|
+
platform = process.platform,
|
|
71
|
+
} = {}) {
|
|
72
|
+
const profile = activeProfile(env);
|
|
73
|
+
const configDirName = env.PI_CONFIG_DIR || CONFIG_DIR_NAME;
|
|
74
|
+
const baseConfigRoot = path.join(homedir, configDirName);
|
|
75
|
+
const configRoot = profile ? path.join(baseConfigRoot, 'profiles', profile) : baseConfigRoot;
|
|
76
|
+
|
|
77
|
+
const defaultAgentDir = path.join(configRoot, 'agent');
|
|
78
|
+
// A named profile pins the agent dir to the profile root; PI_CODING_AGENT_DIR applies only in
|
|
79
|
+
// default mode (dirs.ts: `const agentDirOverride = profile ? undefined : options.agentDirOverride`).
|
|
80
|
+
const agentDirOverride = profile ? undefined : env.PI_CODING_AGENT_DIR;
|
|
81
|
+
const agentDir = agentDirOverride ? path.resolve(agentDirOverride) : defaultAgentDir;
|
|
82
|
+
|
|
83
|
+
let dataBase = agentDir;
|
|
84
|
+
if ((platform === 'linux' || platform === 'darwin') && agentDir === defaultAgentDir) {
|
|
85
|
+
const xdgDataHome = env.XDG_DATA_HOME;
|
|
86
|
+
if (xdgDataHome) {
|
|
87
|
+
const appRoot = path.join(xdgDataHome, APP_NAME);
|
|
88
|
+
const candidate = profile ? path.join(appRoot, 'profiles', profile) : appRoot;
|
|
89
|
+
if (directoryExists(candidate)) dataBase = candidate;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return path.join(dataBase, 'blobs');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** True when `candidate` is the shared blob root or anything inside it. Cleanup uses this as a
|
|
97
|
+
* hard stop: a Zeroshot partition must never resolve into OMP's shared, cross-session CAS. */
|
|
98
|
+
function isInsideOmpBlobsDir(candidate, options = {}) {
|
|
99
|
+
const blobsDir = resolveOmpBlobsDir(options);
|
|
100
|
+
const resolved = path.resolve(candidate);
|
|
101
|
+
return resolved === blobsDir || resolved.startsWith(blobsDir + path.sep);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
APP_NAME,
|
|
106
|
+
CONFIG_DIR_NAME,
|
|
107
|
+
isInsideOmpBlobsDir,
|
|
108
|
+
normalizeProfileName,
|
|
109
|
+
resolveOmpBlobsDir,
|
|
110
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { randomUUID } = require('crypto');
|
|
4
|
+
const { createHash, randomUUID } = require('crypto');
|
|
5
5
|
|
|
6
6
|
const OVERLAY_PREFIX = 'zeroshot-omp-config-';
|
|
7
7
|
const OMP_CONFIG_OVERLAY_DIR_PATTERN = /^zeroshot-omp-config-[A-Za-z0-9_-]+$/u;
|
|
@@ -62,6 +62,13 @@ bash:
|
|
|
62
62
|
thresholdMs: 60000
|
|
63
63
|
`;
|
|
64
64
|
|
|
65
|
+
// Identity of the overlay *content*, not of any one temp file. A resumed session was produced
|
|
66
|
+
// under whatever workflow-altering defaults this body pinned; if the body changes (a Zeroshot
|
|
67
|
+
// upgrade retunes task.*/memory/advisor/async behaviour), continuing an old transcript under the
|
|
68
|
+
// new rules is execution drift, so this digest is part of the OMP execution fingerprint recorded
|
|
69
|
+
// with every resumable session (src/omp-execution-fingerprint.js).
|
|
70
|
+
const OMP_CONFIG_OVERLAY_DIGEST = `sha256:${createHash('sha256').update(OVERLAY_BODY, 'utf8').digest('hex')}`;
|
|
71
|
+
|
|
65
72
|
function createOmpConfigOverlay() {
|
|
66
73
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), OVERLAY_PREFIX), { mode: 0o700 });
|
|
67
74
|
try {
|
|
@@ -89,6 +96,7 @@ function isCanonicalOmpConfigOverlayDirectory(overlayDir) {
|
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
module.exports = {
|
|
99
|
+
OMP_CONFIG_OVERLAY_DIGEST,
|
|
92
100
|
OMP_CONFIG_OVERLAY_DIR_PATTERN,
|
|
93
101
|
OMP_CONFIG_OVERLAY_FILE_PATTERN,
|
|
94
102
|
createOmpConfigOverlay,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The `executionFingerprint` recorded with every resumable OMP session (issue #866).
|
|
2
|
+
//
|
|
3
|
+
// A session transcript is only safely continuable under the same execution contract that produced
|
|
4
|
+
// it. This digest binds that contract: the pinned OMP release, the Zeroshot config overlay's
|
|
5
|
+
// content, the requested Zeroshot selectors (`--model`, `--thinking`, `--approval-mode`), and the
|
|
6
|
+
// concrete provider/model/thinking level OMP actually reported for the turn. Any of those drifting
|
|
7
|
+
// between the recording turn and a resume attempt — a Zeroshot upgrade that retunes the overlay, a
|
|
8
|
+
// changed level mapping, an alias resolving to a different concrete model, a different thinking
|
|
9
|
+
// level — makes the fingerprints differ, and the continuation is refused before the prompt.
|
|
10
|
+
const { createHash } = require('crypto');
|
|
11
|
+
const { OMP_CONFIG_OVERLAY_DIGEST } = require('./omp-config-overlay');
|
|
12
|
+
|
|
13
|
+
/** Value of `--flag <value>` in an argv array, or '' when the flag is absent. */
|
|
14
|
+
function flagValue(args, flag) {
|
|
15
|
+
if (!Array.isArray(args)) return '';
|
|
16
|
+
const index = args.indexOf(flag);
|
|
17
|
+
if (index < 0 || index + 1 >= args.length) return '';
|
|
18
|
+
const value = args[index + 1];
|
|
19
|
+
return typeof value === 'string' ? value : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The Zeroshot-requested half of the contract, readable from the command spec alone. */
|
|
23
|
+
function requestedExecutionSelectors(commandSpec) {
|
|
24
|
+
const args = commandSpec?.args;
|
|
25
|
+
return {
|
|
26
|
+
modelSelector: flagValue(args, '--model'),
|
|
27
|
+
thinkingSelector: flagValue(args, '--thinking'),
|
|
28
|
+
approvalMode: flagValue(args, '--approval-mode'),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} params
|
|
34
|
+
* @param {string} params.expectedVersion pinned OMP release (OMP_SUPPORTED_VERSION)
|
|
35
|
+
* @param {object} params.commandSpec the spec OMP was actually spawned with
|
|
36
|
+
* @param {object} params.evidence OMP's reported session evidence (selectedProvider/Model, thinkingLevel)
|
|
37
|
+
* @param {string} [params.configOverlayDigest] injectable for tests only
|
|
38
|
+
* @returns {string} `sha256:<64-lower-hex>`
|
|
39
|
+
*/
|
|
40
|
+
function computeOmpExecutionFingerprint({
|
|
41
|
+
expectedVersion,
|
|
42
|
+
commandSpec,
|
|
43
|
+
evidence,
|
|
44
|
+
configOverlayDigest = OMP_CONFIG_OVERLAY_DIGEST,
|
|
45
|
+
}) {
|
|
46
|
+
const fields = {
|
|
47
|
+
ompSupportedVersion: String(expectedVersion ?? ''),
|
|
48
|
+
configOverlayDigest: String(configOverlayDigest ?? ''),
|
|
49
|
+
...requestedExecutionSelectors(commandSpec),
|
|
50
|
+
observedProvider: String(evidence?.selectedProvider ?? ''),
|
|
51
|
+
observedModel: String(evidence?.selectedModel ?? ''),
|
|
52
|
+
observedThinkingLevel: String(evidence?.thinkingLevel ?? ''),
|
|
53
|
+
};
|
|
54
|
+
const stable = {};
|
|
55
|
+
for (const key of Object.keys(fields).sort()) stable[key] = fields[key];
|
|
56
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(stable), 'utf8').digest('hex')}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
computeOmpExecutionFingerprint,
|
|
61
|
+
requestedExecutionSelectors,
|
|
62
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Non-configurable bounds for OMP session partition verification (src/omp-session-verifier.js).
|
|
2
|
+
// Every value is pinned exactly as specified by issue #866; do not derive these from settings or
|
|
3
|
+
// environment — a configurable ceiling here would let a compromised/misbehaving OMP process (or a
|
|
4
|
+
// hostile resumed partition) negotiate its own verification budget.
|
|
5
|
+
const OMP_SESSION_LIMITS = Object.freeze({
|
|
6
|
+
maxSessionBytes: 268435456,
|
|
7
|
+
maxSessionRecords: 1000000,
|
|
8
|
+
maxArtifactEntries: 4096,
|
|
9
|
+
maxArtifactDepth: 16,
|
|
10
|
+
maxRelativePathBytes: 4096,
|
|
11
|
+
maxArtifactFileBytes: 268435456,
|
|
12
|
+
maxArtifactAggregateBytes: 536870912,
|
|
13
|
+
maxBlobReferences: 4096,
|
|
14
|
+
maxReferencedBlobBytes: 67108864,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The largest single JSONL record the verifier will buffer, DERIVED from the constants above
|
|
19
|
+
* rather than chosen — it is `maxReferencedBlobBytes`, and it is not a new knob (there is nothing
|
|
20
|
+
* to configure and no caller may override it).
|
|
21
|
+
*
|
|
22
|
+
* Why a per-record bound is needed at all: `maxSessionBytes` bounds the *file*, not a line within
|
|
23
|
+
* it. A hostile 256 MiB session with no newline in it is one record, and buffering it would cost
|
|
24
|
+
* the raw bytes, a concatenated copy, a UTF-16 string for JSON.parse, and the parsed value — a
|
|
25
|
+
* multi-hundred-megabyte spike driven entirely by the attacker's choice of where to put newlines.
|
|
26
|
+
*
|
|
27
|
+
* Why this value: `maxReferencedBlobBytes` is the issue's own answer to "how large may one
|
|
28
|
+
* addressable unit of session content be". OMP externalizes anything bigger than a message to the
|
|
29
|
+
* shared CAS store (blob-store.ts) and leaves only a 76-byte `blob:sha256:<hex>` reference in the
|
|
30
|
+
* record, so a legitimate record is orders of magnitude smaller than this; the bound exists to cap
|
|
31
|
+
* the pathological case, not to constrain real transcripts.
|
|
32
|
+
*
|
|
33
|
+
* Remaining allocation, exactly: verification buffers at most MAX_SESSION_RECORD_BYTES of raw
|
|
34
|
+
* record bytes, and `JSON.parse` necessarily materializes that record as one UTF-16 string plus its
|
|
35
|
+
* parsed value. Peak per-record cost is therefore O(MAX_SESSION_RECORD_BYTES) and independent of
|
|
36
|
+
* `maxSessionBytes`, the record count, and the file's newline placement. Nothing else in the
|
|
37
|
+
* verifier accumulates session, artifact, or blob bytes.
|
|
38
|
+
*/
|
|
39
|
+
const MAX_SESSION_RECORD_BYTES = OMP_SESSION_LIMITS.maxReferencedBlobBytes;
|
|
40
|
+
|
|
41
|
+
module.exports = { OMP_SESSION_LIMITS, MAX_SESSION_RECORD_BYTES };
|