@the-open-engine/zeroshot 6.23.0 → 6.25.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.
- package/cli/index.js +37 -2
- package/docker/zeroshot-cluster/Dockerfile +7 -0
- 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-release.d.ts +3 -0
- package/lib/agent-cli-provider/omp-release.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp-release.js +20 -1
- package/lib/agent-cli-provider/omp-release.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 +20 -8
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +56 -9
- 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/lib/docker-config.js +122 -5
- package/package.json +2 -2
- package/src/agent/agent-lifecycle.js +78 -3
- package/src/agent/agent-task-executor.js +72 -3
- package/src/agent/provider-session.js +112 -2
- package/src/agent-cli-provider/adapters/omp.ts +41 -11
- package/src/agent-cli-provider/omp-release.ts +26 -0
- 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 +102 -20
- package/src/agent-cli-provider/types.ts +4 -0
- package/src/isolation-manager.js +535 -89
- 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 +17 -0
- package/src/omp-session-partition.js +297 -0
- package/src/omp-session-verifier.js +576 -0
- package/src/orchestrator.js +11 -1
- package/src/preflight.js +15 -2
- package/task-lib/commands/clean.js +23 -0
- package/task-lib/commands/resume.js +42 -0
- package/task-lib/commands/run.js +65 -0
- package/task-lib/omp-session-cleanup.js +160 -0
- package/task-lib/omp-session-ownership-schema.js +262 -0
- package/task-lib/omp-session-ownership.js +332 -0
- package/task-lib/omp-storage-root.js +35 -0
- package/task-lib/rpc-watcher.js +332 -2
- package/task-lib/runner.js +195 -4
- package/task-lib/store.js +42 -7
|
@@ -17,6 +17,7 @@ const { getNestedExecutionRegistry, TaskExecutionHandle } = require('./task-exec
|
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const { parseProviderChunk, getProvider } = require('../providers');
|
|
19
19
|
const { getTask, getTaskBySpawnOwnershipToken } = require('../../task-lib/store.js');
|
|
20
|
+
const { OMP_SESSIONLESS_ENV } = require('../../task-lib/omp-storage-root.js');
|
|
20
21
|
const { loadSettings } = require('../../lib/settings.js');
|
|
21
22
|
const { getDefaultProviderId } = require('../../lib/provider-names');
|
|
22
23
|
const { resolveClaudeAuth } = require('../../lib/settings/claude-auth.js');
|
|
@@ -46,6 +47,7 @@ const {
|
|
|
46
47
|
} = require('../task-spawn-cleanup-ownership');
|
|
47
48
|
const {
|
|
48
49
|
providerSessionFromCompletedTask,
|
|
50
|
+
resolveAgentProviderSession,
|
|
49
51
|
resolveAgentResumeSessionId,
|
|
50
52
|
validateCompletedResumeIdentity,
|
|
51
53
|
} = require('./provider-session');
|
|
@@ -722,14 +724,48 @@ function buildTaskRunArgs({
|
|
|
722
724
|
args.push(mcpArg);
|
|
723
725
|
}
|
|
724
726
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
args.push('--resume',
|
|
727
|
+
if (providerName === 'omp') {
|
|
728
|
+
const ompResumeArg = buildOmpResumeArg(agent, providerName);
|
|
729
|
+
if (ompResumeArg) args.push('--omp-resume', ompResumeArg);
|
|
730
|
+
} else {
|
|
731
|
+
const resumeSessionId = resolveAgentResumeSessionId(agent, providerName);
|
|
732
|
+
if (resumeSessionId) {
|
|
733
|
+
args.push('--resume', resumeSessionId);
|
|
734
|
+
}
|
|
728
735
|
}
|
|
729
736
|
|
|
730
737
|
return args;
|
|
731
738
|
}
|
|
732
739
|
|
|
740
|
+
/**
|
|
741
|
+
* OMP never accepts a bare `--resume <sessionId>` (see failClosedUnsupportedSessionControl in
|
|
742
|
+
* adapters/omp.ts): the child `zeroshot task run` process needs the *complete* committed tuple so
|
|
743
|
+
* it can re-check it against the prior owner's persisted row and re-verify identity/manifest/
|
|
744
|
+
* fingerprint drift before ever spawning OMP.
|
|
745
|
+
*
|
|
746
|
+
* `priorOwnerTaskId` is what turns this from an assertion into a cross-check: the child resolves
|
|
747
|
+
* that row and requires every field below to match it exactly, so a stale or tampered argv
|
|
748
|
+
* descriptor is a conflict that fails closed instead of a claim that is taken at face value.
|
|
749
|
+
* Nothing here is secret — it is the same non-secret shape as task.ompSessionOwnership.session,
|
|
750
|
+
* and it deliberately carries no storage-root or partition path.
|
|
751
|
+
*/
|
|
752
|
+
function buildOmpResumeArg(agent, providerName) {
|
|
753
|
+
const session = resolveAgentProviderSession(agent, providerName);
|
|
754
|
+
const ompSession = session?.ompSession;
|
|
755
|
+
if (!session || !ompSession) return null;
|
|
756
|
+
return JSON.stringify({
|
|
757
|
+
priorOwnerTaskId: session.taskId,
|
|
758
|
+
partitionId: ompSession.partitionId,
|
|
759
|
+
sessionFileName: ompSession.sessionFileName,
|
|
760
|
+
expectedSessionId: session.sessionId,
|
|
761
|
+
expectedSessionFileIdentity: ompSession.sessionFileIdentity,
|
|
762
|
+
expectedArtifactManifestDigest: ompSession.artifactManifestDigest,
|
|
763
|
+
expectedExecutionFingerprint: ompSession.executionFingerprint,
|
|
764
|
+
expectedSelectedProvider: ompSession.selectedProvider,
|
|
765
|
+
expectedSelectedModel: ompSession.selectedModel,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
733
769
|
/**
|
|
734
770
|
* Build the `--mcp-config` args for a task-run invocation, or [] when they don't apply.
|
|
735
771
|
*
|
|
@@ -805,6 +841,7 @@ function buildSpawnEnv(agent, providerName, modelSpec, options = {}) {
|
|
|
805
841
|
|
|
806
842
|
if (clusterId) {
|
|
807
843
|
spawnEnv.ZEROSHOT_CLUSTER_ID = clusterId;
|
|
844
|
+
spawnEnv.ZEROSHOT_AGENT_ID = agent.id;
|
|
808
845
|
const cmdproofRoot = path.join(os.homedir(), '.zeroshot', 'cmdproof', clusterId);
|
|
809
846
|
if (!spawnEnv.CMDPROOF_CACHE_DIR) {
|
|
810
847
|
spawnEnv.CMDPROOF_CACHE_DIR = path.join(cmdproofRoot, 'cache');
|
|
@@ -812,6 +849,13 @@ function buildSpawnEnv(agent, providerName, modelSpec, options = {}) {
|
|
|
812
849
|
if (!spawnEnv.CMDPROOF_KEY_DIR) {
|
|
813
850
|
spawnEnv.CMDPROOF_KEY_DIR = path.join(cmdproofRoot, 'keys');
|
|
814
851
|
}
|
|
852
|
+
// OMP session partitions live under the owning cluster's storageDir (never TASKS_DIR — see
|
|
853
|
+
// task-lib/omp-storage-root.js), forwarded to the spawned `zeroshot task run` child since it
|
|
854
|
+
// has no other way to learn which orchestrator instance owns this cluster.
|
|
855
|
+
if (providerName === 'omp') {
|
|
856
|
+
spawnEnv.ZEROSHOT_OMP_STORAGE_ROOT =
|
|
857
|
+
agent.cluster?.storageDir || path.join(os.homedir(), '.zeroshot');
|
|
858
|
+
}
|
|
815
859
|
}
|
|
816
860
|
|
|
817
861
|
const commandProofs = Array.isArray(agent.config?.commandProofs)
|
|
@@ -1456,6 +1500,25 @@ async function buildCompletionResult({
|
|
|
1456
1500
|
};
|
|
1457
1501
|
}
|
|
1458
1502
|
|
|
1503
|
+
/**
|
|
1504
|
+
* Rebuild the provider-session snapshot from a *freshly re-read* task row.
|
|
1505
|
+
*
|
|
1506
|
+
* The snapshot inside a completion result is necessarily built while the OMP ownership row is
|
|
1507
|
+
* still `provisional` — the detached watcher records verified materialization evidence but defers
|
|
1508
|
+
* the commit decision to the agent's post-hook success boundary — so for provider `omp` it is
|
|
1509
|
+
* stale by construction and always null. agent-lifecycle.js calls this *after* a checked commit
|
|
1510
|
+
* and before publishing TASK_COMPLETED, which is the only point at which a durable, resumable
|
|
1511
|
+
* `ompSession` exists to snapshot.
|
|
1512
|
+
*/
|
|
1513
|
+
function rebuildProviderSessionAfterCommit({ agent, providerName, taskId, logicalSuccess = true }) {
|
|
1514
|
+
return providerSessionFromCompletedTask({
|
|
1515
|
+
agent,
|
|
1516
|
+
providerName,
|
|
1517
|
+
taskInfo: getTask(taskId),
|
|
1518
|
+
logicalSuccess,
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1459
1522
|
function finalizeLogFollow(agent, state) {
|
|
1460
1523
|
if (state.pollInterval) {
|
|
1461
1524
|
clearInterval(state.pollInterval);
|
|
@@ -1886,6 +1949,11 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
1886
1949
|
// only authoritative bridge back to the detached task row in the container.
|
|
1887
1950
|
const isolatedEnv = {
|
|
1888
1951
|
...(providerName === 'claude' ? buildClaudeEnv(modelSpec, { includeAuth: false }) : {}),
|
|
1952
|
+
// Docker is fresh-only for OMP (issue #866): the container filesystem is ephemeral, so a
|
|
1953
|
+
// session partition allocated inside it could never be resumed and its ownership row would be
|
|
1954
|
+
// unreclaimable once the container is removed. This marker makes the in-container
|
|
1955
|
+
// `zeroshot task run` skip partition allocation entirely and launch `--no-session`.
|
|
1956
|
+
...(providerName === 'omp' ? { [OMP_SESSIONLESS_ENV]: '1' } : {}),
|
|
1889
1957
|
[TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken,
|
|
1890
1958
|
};
|
|
1891
1959
|
|
|
@@ -3003,5 +3071,6 @@ module.exports = {
|
|
|
3003
3071
|
parseResultOutput,
|
|
3004
3072
|
buildCompletionResult,
|
|
3005
3073
|
buildTaskRunArgs,
|
|
3074
|
+
rebuildProviderSessionAfterCommit,
|
|
3006
3075
|
killTask,
|
|
3007
3076
|
};
|
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
|
|
4
4
|
const { normalizeProviderName, providerSupportsCapability } = require('../../lib/provider-names');
|
|
5
5
|
const { tryCanonicalMessageSequence } = require('../ledger-sequence');
|
|
6
|
+
const { validateOwnedByTask } = require('../../task-lib/omp-session-ownership-schema.js');
|
|
6
7
|
|
|
7
8
|
const DURABLE_SESSION_BOUNDARY_EVENTS = new Set([
|
|
8
9
|
'TASK_STARTED',
|
|
@@ -44,6 +45,77 @@ function promptIdentity(value) {
|
|
|
44
45
|
return `sha256:${crypto.createHash('sha256').update(String(value)).digest('hex')}`;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
const DECIMAL_STRING = /^(0|[1-9][0-9]*)$/;
|
|
49
|
+
const SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
50
|
+
const SESSION_FILE_NAME = /^[^/\\]+\.jsonl$/;
|
|
51
|
+
const { PARTITION_ID_PATTERN } = require('../omp-session-partition');
|
|
52
|
+
|
|
53
|
+
// Exactly the field set issue #866 fixes for this snapshot — no more, no less. The record is
|
|
54
|
+
// closed in both directions so a stale snapshot written by a different Zeroshot version, or one
|
|
55
|
+
// carrying smuggled extra state, is rejected rather than partially trusted.
|
|
56
|
+
const OMP_SESSION_KEYS = new Set([
|
|
57
|
+
'schemaVersion',
|
|
58
|
+
'partitionId',
|
|
59
|
+
'sessionFileName',
|
|
60
|
+
'sessionFileIdentity',
|
|
61
|
+
'artifactManifestDigest',
|
|
62
|
+
'executionFingerprint',
|
|
63
|
+
'selectedProvider',
|
|
64
|
+
'selectedModel',
|
|
65
|
+
]);
|
|
66
|
+
const IDENTITY_KEYS = new Set(['device', 'inode']);
|
|
67
|
+
|
|
68
|
+
function normalizeIdentity(value) {
|
|
69
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
70
|
+
if (!Object.keys(value).every((key) => IDENTITY_KEYS.has(key))) return null;
|
|
71
|
+
if (!DECIMAL_STRING.test(String(value.device)) || !DECIMAL_STRING.test(String(value.inode))) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return { device: String(value.device), inode: String(value.inode) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The optional providerSession.ompSession field (issue #866): required in addition to the
|
|
79
|
+
* generic tuple above for provider 'omp', absent for every other provider. Every digest here is
|
|
80
|
+
* sha256:<64-lower-hex>; every device/inode is a canonical unsigned decimal string. Never carries
|
|
81
|
+
* storage-root or partition paths — those stay in task.ompSessionOwnership, not the agent
|
|
82
|
+
* snapshot; the partition is re-derived from the owner row at resume time.
|
|
83
|
+
*/
|
|
84
|
+
function normalizeOmpSession(value) {
|
|
85
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (!Object.keys(value).every((key) => OMP_SESSION_KEYS.has(key))) return null;
|
|
89
|
+
const sessionFileIdentity = normalizeIdentity(value.sessionFileIdentity);
|
|
90
|
+
if (
|
|
91
|
+
value.schemaVersion !== 1 ||
|
|
92
|
+
!normalizeNonEmptyString(value.partitionId) ||
|
|
93
|
+
!PARTITION_ID_PATTERN.test(value.partitionId) ||
|
|
94
|
+
!normalizeNonEmptyString(value.sessionFileName) ||
|
|
95
|
+
!SESSION_FILE_NAME.test(value.sessionFileName) ||
|
|
96
|
+
value.sessionFileName === '.jsonl' ||
|
|
97
|
+
!sessionFileIdentity ||
|
|
98
|
+
typeof value.artifactManifestDigest !== 'string' ||
|
|
99
|
+
!SHA256_DIGEST.test(value.artifactManifestDigest) ||
|
|
100
|
+
typeof value.executionFingerprint !== 'string' ||
|
|
101
|
+
!SHA256_DIGEST.test(value.executionFingerprint) ||
|
|
102
|
+
!normalizeNonEmptyString(value.selectedProvider) ||
|
|
103
|
+
!normalizeNonEmptyString(value.selectedModel)
|
|
104
|
+
) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
schemaVersion: 1,
|
|
109
|
+
partitionId: value.partitionId,
|
|
110
|
+
sessionFileName: value.sessionFileName,
|
|
111
|
+
sessionFileIdentity,
|
|
112
|
+
artifactManifestDigest: value.artifactManifestDigest,
|
|
113
|
+
executionFingerprint: value.executionFingerprint,
|
|
114
|
+
selectedProvider: value.selectedProvider,
|
|
115
|
+
selectedModel: value.selectedModel,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
47
119
|
function supportsSessionResume(providerName) {
|
|
48
120
|
try {
|
|
49
121
|
return providerSupportsCapability(providerName, 'sessionResume');
|
|
@@ -68,6 +140,8 @@ function normalizeProviderSession(value) {
|
|
|
68
140
|
const contextSequence = normalizeCursor(value.contextSequence);
|
|
69
141
|
const guidanceSequence = normalizeNullableCursor(value.guidanceSequence);
|
|
70
142
|
const normalizedPromptIdentity = normalizePromptIdentity(value.promptIdentity);
|
|
143
|
+
const hasOmpSession = Object.hasOwn(value, 'ompSession');
|
|
144
|
+
const normalizedOmpSession = hasOmpSession ? normalizeOmpSession(value.ompSession) : null;
|
|
71
145
|
|
|
72
146
|
if (
|
|
73
147
|
!provider ||
|
|
@@ -84,7 +158,8 @@ function normalizeProviderSession(value) {
|
|
|
84
158
|
(value.guidanceSequence !== null && guidanceSequence === null) ||
|
|
85
159
|
!Object.hasOwn(value, 'promptIdentity') ||
|
|
86
160
|
normalizedPromptIdentity === undefined ||
|
|
87
|
-
!supportsSessionResume(provider)
|
|
161
|
+
!supportsSessionResume(provider) ||
|
|
162
|
+
(provider === 'omp' ? normalizedOmpSession === null : hasOmpSession)
|
|
88
163
|
) {
|
|
89
164
|
return null;
|
|
90
165
|
}
|
|
@@ -100,6 +175,7 @@ function normalizeProviderSession(value) {
|
|
|
100
175
|
contextSequence,
|
|
101
176
|
guidanceSequence,
|
|
102
177
|
promptIdentity: normalizedPromptIdentity,
|
|
178
|
+
...(provider === 'omp' ? { ompSession: normalizedOmpSession } : {}),
|
|
103
179
|
};
|
|
104
180
|
}
|
|
105
181
|
|
|
@@ -169,6 +245,29 @@ function validateCompletedResumeIdentity(taskInfo) {
|
|
|
169
245
|
: 'Provider continuation did not confirm the requested session identity';
|
|
170
246
|
}
|
|
171
247
|
|
|
248
|
+
/** taskInfo.ompSessionOwnership is the authoritative, watcher-committed continuation evidence for
|
|
249
|
+
* provider 'omp'; a provisional/cleanup-required/missing record means this task never durably
|
|
250
|
+
* proved a resumable session, regardless of what the generic sessionId/resumeIdentityVerified
|
|
251
|
+
* columns say (rpc-watcher.js never populates those — they're the stdout-parsing watchers' path).
|
|
252
|
+
* The record is re-fenced to this exact task row: an ownership object naming a different owner is
|
|
253
|
+
* not this task's continuation evidence, however well-formed it is. */
|
|
254
|
+
function ompSessionFromCompletedTask(taskInfo) {
|
|
255
|
+
const ownership = validateOwnedByTask(taskInfo?.ompSessionOwnership ?? null, taskInfo?.id);
|
|
256
|
+
if (!ownership || ownership.state !== 'committed' || !ownership.session) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
schemaVersion: 1,
|
|
261
|
+
partitionId: ownership.partitionId,
|
|
262
|
+
sessionFileName: ownership.session.fileName,
|
|
263
|
+
sessionFileIdentity: ownership.session.fileIdentity,
|
|
264
|
+
artifactManifestDigest: ownership.session.artifactManifestDigest,
|
|
265
|
+
executionFingerprint: ownership.session.executionFingerprint,
|
|
266
|
+
selectedProvider: ownership.session.selectedProvider,
|
|
267
|
+
selectedModel: ownership.session.selectedModel,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
172
271
|
function providerSessionFromCompletedTask({
|
|
173
272
|
agent,
|
|
174
273
|
providerName,
|
|
@@ -192,7 +291,16 @@ function providerSessionFromCompletedTask({
|
|
|
192
291
|
return null;
|
|
193
292
|
}
|
|
194
293
|
|
|
195
|
-
const
|
|
294
|
+
const isOmp = provider === 'omp';
|
|
295
|
+
const ompOwnership = isOmp
|
|
296
|
+
? validateOwnedByTask(taskInfo.ompSessionOwnership ?? null, taskInfo.id)
|
|
297
|
+
: null;
|
|
298
|
+
const ompSession = isOmp ? ompSessionFromCompletedTask(taskInfo) : null;
|
|
299
|
+
// rpc-watcher.js never populates the generic sessionId column; the OMP-observed session ID
|
|
300
|
+
// committed alongside ompSession is the one authoritative identity for this provider.
|
|
301
|
+
const sessionId = isOmp
|
|
302
|
+
? normalizeNonEmptyString(ompOwnership?.state === 'committed' ? ompOwnership.session?.sessionId : null)
|
|
303
|
+
: normalizeNonEmptyString(taskInfo.sessionId);
|
|
196
304
|
const taskId = normalizeNonEmptyString(taskInfo.id);
|
|
197
305
|
const generation = agent?.iteration;
|
|
198
306
|
const agentId = normalizeNonEmptyString(agent?.id);
|
|
@@ -207,6 +315,7 @@ function providerSessionFromCompletedTask({
|
|
|
207
315
|
contextSequence: agent?.currentContextSequence,
|
|
208
316
|
guidanceSequence: agent?.currentGuidanceSequence ?? null,
|
|
209
317
|
promptIdentity: agent?.currentPromptIdentity ?? null,
|
|
318
|
+
...(isOmp ? { ompSession } : {}),
|
|
210
319
|
});
|
|
211
320
|
}
|
|
212
321
|
|
|
@@ -264,6 +373,7 @@ function restoreAgentProviderSession({ agent, savedState, messageBus, clusterId
|
|
|
264
373
|
}
|
|
265
374
|
|
|
266
375
|
module.exports = {
|
|
376
|
+
agentCanReuseSession,
|
|
267
377
|
agentWorkspaceProvenance,
|
|
268
378
|
normalizeProviderSession,
|
|
269
379
|
promptIdentity,
|
|
@@ -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
|
}
|
|
@@ -84,3 +84,29 @@ export function findOmpReleaseAsset(platform: string): OmpReleaseAsset | undefin
|
|
|
84
84
|
export function ompReleaseAssetDownloadUrl(asset: OmpReleaseAsset): string {
|
|
85
85
|
return `${OMP_RELEASE_DOWNLOAD_BASE_URL}/${asset.name}`;
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
// Docker isolation runs the cluster image as linux/amd64 only (see AGENTS.md OMP Docker
|
|
89
|
+
// section); the base image's AWS/Terraform/kubectl/Helm/Infracost/TFLint/tfsec layers are
|
|
90
|
+
// hard-coded x86-64 assets, so this cannot yet claim native arm64.
|
|
91
|
+
export const OMP_DOCKER_PLATFORM = 'linux/amd64' as const;
|
|
92
|
+
export const OMP_DOCKER_RELEASE_PLATFORM = 'linux-x64' as const satisfies OmpReleasePlatform;
|
|
93
|
+
|
|
94
|
+
const OMP_DOCKER_RELEASE_ASSET = findOmpReleaseAsset(OMP_DOCKER_RELEASE_PLATFORM);
|
|
95
|
+
if (!OMP_DOCKER_RELEASE_ASSET) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`No OMP release asset found for Docker platform "${OMP_DOCKER_RELEASE_PLATFORM}"`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const OMP_DOCKER_RELEASE_URL = ompReleaseAssetDownloadUrl(OMP_DOCKER_RELEASE_ASSET);
|
|
102
|
+
const OMP_DOCKER_RELEASE_SHA256 = OMP_DOCKER_RELEASE_ASSET.sha256;
|
|
103
|
+
|
|
104
|
+
// Digest-verified install for the Docker image variant only. Downloads the pinned release asset,
|
|
105
|
+
// verifies its SHA-256 before install, and asserts `omp --version` matches exactly — never
|
|
106
|
+
// `latest`, never trusting the tag URL alone.
|
|
107
|
+
export const OMP_DOCKER_INSTALL_COMMAND: string =
|
|
108
|
+
`set -eu; curl -fsSL --retry 3 -o /tmp/omp "${OMP_DOCKER_RELEASE_URL}"; ` +
|
|
109
|
+
`printf '%s /tmp/omp\\n' '${OMP_DOCKER_RELEASE_SHA256}' | sha256sum -c -; ` +
|
|
110
|
+
`install -m 0755 /tmp/omp /usr/local/bin/omp; rm -f /tmp/omp; ` +
|
|
111
|
+
`v="$(omp --version 2>&1 | head -n1 | tr -dc '0-9.')"; ` +
|
|
112
|
+
`[ "$v" = "${OMP_SUPPORTED_VERSION}" ] || { echo "omp --version mismatch: $v" >&2; exit 1; }`;
|
|
@@ -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;
|