@xemahq/worker-runtime-opencode 0.3.0 → 0.4.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.
@@ -14,7 +14,6 @@ export interface OpencodeSessionBundlePayload {
14
14
  };
15
15
  readonly subAgents: readonly OpencodeBundleSubAgent[];
16
16
  readonly modelOverride?: string;
17
- readonly mcpConfig?: string;
18
17
  readonly serviceToken?: string;
19
18
  readonly staticFingerprint: string;
20
19
  readonly bindingRevision: string;
@@ -1 +1 @@
1
- {"version":3,"file":"session-bundle.d.ts","sourceRoot":"","sources":["../../src/lib/session-bundle.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACd,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAQxD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAMD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,QAAQ,CAAC,SAAS,EAAE,SAAS,sBAAsB,EAAE,CAAC;IACtD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAW5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAgCD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,gBAAgB,GACtB,MAAM,CAaR;AAMD,wBAAgB,6BAA6B,CAC3C,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,mBAAmB,GAC1B,MAAM,CAaR;AAQD,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,gBAAgB,EACvB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,mBAAmB,EAC3B,YAAY,EAAE,MAAM,EACpB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC;IACD,QAAQ,CAAC,OAAO,EAAE,4BAA4B,CAAC;IAC/C,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC,CAsDA;AAQD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,aAAa,GACpB,4BAA4B,CAqB9B"}
1
+ {"version":3,"file":"session-bundle.d.ts","sourceRoot":"","sources":["../../src/lib/session-bundle.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACd,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAQxD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAMD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,QAAQ,CAAC,SAAS,EAAE,SAAS,sBAAsB,EAAE,CAAC;IACtD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAWhC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AA4BD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,gBAAgB,GACtB,MAAM,CAwBR;AAWD,wBAAgB,6BAA6B,CAC3C,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,mBAAmB,GAC1B,MAAM,CAOR;AAQD,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,gBAAgB,EACvB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,mBAAmB,EAC3B,YAAY,EAAE,MAAM,EACpB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC;IACD,QAAQ,CAAC,OAAO,EAAE,4BAA4B,CAAC;IAC/C,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC,CAqDA;AAQD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,aAAa,GACpB,4BAA4B,CAqB9B"}
@@ -4,43 +4,26 @@ exports.computeStaticBundleFingerprint = computeStaticBundleFingerprint;
4
4
  exports.computeSessionBindingRevision = computeSessionBindingRevision;
5
5
  exports.buildOpencodeSessionBundle = buildOpencodeSessionBundle;
6
6
  exports.decodeOpencodeSessionBundle = decodeOpencodeSessionBundle;
7
- const node_crypto_1 = require("node:crypto");
8
- function canonicalize(value) {
9
- if (value === null || typeof value !== 'object')
10
- return value;
11
- if (Array.isArray(value))
12
- return value.map((v) => canonicalize(v));
13
- const obj = value;
14
- const out = {};
15
- for (const key of Object.keys(obj).sort()) {
16
- const v = obj[key];
17
- if (v === undefined)
18
- continue;
19
- out[key] = canonicalize(v);
20
- }
21
- return out;
22
- }
7
+ const session_bundle_identity_1 = require("@xemahq/kernel-contracts/session-bundle-identity");
23
8
  function computeStaticBundleFingerprint(input) {
24
- const sortedSubAgents = [...input.subAgents].sort();
25
- const canonical = canonicalize({
26
- primaryAgent: input.primaryAgent,
27
- subAgents: sortedSubAgents,
9
+ if (!input.runtimeConfigBase64) {
10
+ throw new Error('computeStaticBundleFingerprint: runtimeConfigBase64 is required — a ' +
11
+ 'bundle with no composed config has no meaningful template identity, ' +
12
+ 'and hashing its absence collides every such bundle onto one key.');
13
+ }
14
+ return (0, session_bundle_identity_1.buildStaticBundleFingerprint)({
15
+ primaryAgent: { slug: input.primaryAgent },
16
+ subAgents: input.subAgents.map((slug) => ({ slug })),
28
17
  opencodeConfigBase64: input.runtimeConfigBase64,
29
- mcpConfig: input.mcpConfig,
30
18
  });
31
- const hash = (0, node_crypto_1.createHash)('sha256');
32
- hash.update(JSON.stringify(canonical));
33
- return hash.digest('hex');
34
19
  }
35
20
  function computeSessionBindingRevision(allocationId, caller) {
36
- return (0, node_crypto_1.createHash)('sha256')
37
- .update(JSON.stringify(canonicalize({
21
+ return (0, session_bundle_identity_1.buildSessionBindingRevision)({
38
22
  allocationId,
39
23
  orgId: caller.orgId,
40
24
  projectId: caller.projectId,
41
- sessionId: caller.sessionId,
42
- })))
43
- .digest('hex');
25
+ ...(caller.sessionId ? { sessionId: caller.sessionId } : {}),
26
+ });
44
27
  }
45
28
  function buildOpencodeSessionBundle(input, allocationId, caller, bindingEpoch, workerServiceToken) {
46
29
  if (!input.primaryAgent.trim()) {
@@ -71,7 +54,6 @@ function buildOpencodeSessionBundle(input, allocationId, caller, bindingEpoch, w
71
54
  ...(input.modelOverride === undefined
72
55
  ? {}
73
56
  : { modelOverride: input.modelOverride }),
74
- ...(input.mcpConfig === undefined ? {} : { mcpConfig: input.mcpConfig }),
75
57
  ...(workerServiceToken ? { serviceToken: workerServiceToken } : {}),
76
58
  };
77
59
  const payloadJson = JSON.stringify(payload);
@@ -1 +1 @@
1
- {"version":3,"file":"session-bundle.js","sourceRoot":"","sources":["../../src/lib/session-bundle.ts"],"names":[],"mappings":";;AA4FA,wEAeC;AAMD,sEAgBC;AAQD,gEA+DC;AAQD,kEAuBC;AA1ND,6CAAyC;AAsDzC,SAAS,YAAY,CAAC,KAAc;IAClC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC1C,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACnB,IAAI,CAAC,KAAK,SAAS;YAAE,SAAS;QAC9B,GAAG,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAcD,SAAgB,8BAA8B,CAC5C,KAAuB;IAEvB,MAAM,eAAe,GAAG,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,MAAM,SAAS,GAAG,YAAY,CAAC;QAC7B,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,SAAS,EAAE,eAAe;QAG1B,oBAAoB,EAAE,KAAK,CAAC,mBAAmB;QAC/C,SAAS,EAAE,KAAK,CAAC,SAAS;KAC3B,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;IACvC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAMD,SAAgB,6BAA6B,CAC3C,YAAoB,EACpB,MAA2B;IAE3B,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC;SACxB,MAAM,CACL,IAAI,CAAC,SAAS,CACZ,YAAY,CAAC;QACX,YAAY;QACZ,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC,CACH,CACF;SACA,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC;AAQD,SAAgB,0BAA0B,CACxC,KAAuB,EACvB,YAAoB,EACpB,MAA2B,EAC3B,YAAoB,EACpB,kBAAsC;IAKtC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,oBAAoB,GAAG,KAAK,CAAC,mBAAmB,IAAI,EAAE,CAAC;IAC7D,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,gJAAgJ,CACjJ,CAAC;IACJ,CAAC;IACD,MAAM,iBAAiB,GAAG,8BAA8B,CAAC,KAAK,CAAC,CAAC;IAChE,MAAM,eAAe,GAAG,6BAA6B,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAE5E,MAAM,SAAS,GAAsC,KAAK,CAAC,SAAS,CAAC,GAAG,CACtE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CACrB,CAAC;IAOF,MAAM,OAAO,GAAiC;QAC5C,YAAY;QACZ,MAAM;QACN,oBAAoB;QACpB,YAAY,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,YAAY,EAAE;QAC1C,SAAS;QACT,iBAAiB;QACjB,eAAe;QACf,YAAY;QACZ,GAAG,CAAC,KAAK,CAAC,aAAa,KAAK,SAAS;YACnC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa,EAAE,CAAC;QAC3C,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;QAKxE,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,kBAAkB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpE,CAAC;IAEF,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC1E,OAAO;QACL,OAAO;QACP,MAAM,EAAE,EAAE,WAAW,EAAE,iBAAiB,EAAE,aAAa,EAAE;KAC1D,CAAC;AACJ,CAAC;AAQD,SAAgB,2BAA2B,CACzC,MAAqB;IAErB,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC1E,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,MAAM,IAAI,KAAK,CACb,0EAA0E,GAAG,EAAE,CAChF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,GAAG,GAAG,MAAkD,CAAC;IAC/D,IAAI,GAAG,CAAC,iBAAiB,KAAK,MAAM,CAAC,WAAW,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CACb,8BAA8B,MAAM,CAAC,WAAW,2DAA2D,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,yCAAyC,CAClL,CAAC;IACJ,CAAC;IACD,OAAO,MAAsC,CAAC;AAChD,CAAC"}
1
+ {"version":3,"file":"session-bundle.js","sourceRoot":"","sources":["../../src/lib/session-bundle.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/worker-runtime-opencode",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Opencode WorkerRuntime driver — wraps the workspace-proxy control-session handshake and the opencode SSE event stream behind the kernel WorkerRuntime contract.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
@@ -18,8 +18,7 @@
18
18
  "main": "dist/index.js",
19
19
  "types": "dist/index.d.ts",
20
20
  "files": [
21
- "dist",
22
- "src"
21
+ "dist"
23
22
  ],
24
23
  "devDependencies": {
25
24
  "@types/jest": "^30.0.0",
@@ -28,7 +27,7 @@
28
27
  "prettier": "3.6.2",
29
28
  "ts-jest": "^29.4.9",
30
29
  "typescript": "5.9.3",
31
- "@xemahq/kernel-contracts": "^8.0.0"
30
+ "@xemahq/kernel-contracts": "^11.2.0"
32
31
  },
33
32
  "exports": {
34
33
  ".": {
@@ -42,7 +41,7 @@
42
41
  }
43
42
  },
44
43
  "peerDependencies": {
45
- "@xemahq/kernel-contracts": ">=1.0.0"
44
+ "@xemahq/kernel-contracts": ">=11.2.0"
46
45
  },
47
46
  "scripts": {
48
47
  "clean": "rm -rf dist",
package/src/index.ts DELETED
@@ -1,28 +0,0 @@
1
- // ═══════════════════════════════════════════════════════════════════════════
2
- // ── @xemahq/worker-runtime-opencode — barrel export ──
3
- //
4
- // opencode driver behind the
5
- // `@xemahq/kernel-contracts/worker-runtime` interface. The orchestrator and
6
- // session-launch services depend on THIS package's `WorkerRuntime` API;
7
- // they never reach into `biomes/workspace-proxy/api/workspace-proxy/` or `apps/opencode-pool-api/`.
8
- // ═══════════════════════════════════════════════════════════════════════════
9
-
10
- export {
11
- OpencodeWorkerRuntime,
12
- OPENCODE_CONNECTOR_KIND,
13
- type OpencodePodSpec,
14
- } from './lib/opencode-worker-runtime';
15
- export { createOpencodeWorkerRuntime } from './lib/factory';
16
- export {
17
- buildOpencodeSessionBundle,
18
- computeSessionBindingRevision,
19
- computeStaticBundleFingerprint,
20
- decodeOpencodeSessionBundle,
21
- type OpencodeBundleSubAgent,
22
- type OpencodeSessionBundlePayload,
23
- } from './lib/session-bundle';
24
- export type {
25
- BundleCallerContext,
26
- OpencodeRuntimeConfig,
27
- WorkerRef,
28
- } from './lib/worker-ref';
@@ -1,56 +0,0 @@
1
- // ═══════════════════════════════════════════════════════════════════════════
2
- // ── OpencodeWorkerRuntime — factory ──
3
- //
4
- // Single entry-point the orchestrator calls per allocated worker. Keeping
5
- // construction behind a factory makes future test-injection seams (mock
6
- // fetch, mock SSE body) trivial without exposing the class constructor's
7
- // shape to consumers.
8
- // ═══════════════════════════════════════════════════════════════════════════
9
-
10
- import { OpencodeWorkerRuntime } from './opencode-worker-runtime';
11
- import type { OpencodeRuntimeConfig } from './worker-ref';
12
-
13
- /**
14
- * Construct an `OpencodeWorkerRuntime` bound to a specific worker.
15
- *
16
- * The orchestrator calls this once `workload-runtime-api` returns a
17
- * scheduled worker + the per-allocation service token has been minted.
18
- */
19
- export function createOpencodeWorkerRuntime(
20
- config: OpencodeRuntimeConfig,
21
- ): OpencodeWorkerRuntime {
22
- validate(config);
23
- return new OpencodeWorkerRuntime(config);
24
- }
25
-
26
- function validate(config: OpencodeRuntimeConfig): void {
27
- const ref = config.workerRef;
28
- if (!ref.workerId.trim()) {
29
- throw new Error('OpencodeRuntimeConfig.workerRef.workerId is required.');
30
- }
31
- if (!ref.controlPlaneUrl.trim()) {
32
- throw new Error('OpencodeRuntimeConfig.workerRef.controlPlaneUrl is required.');
33
- }
34
- if (!ref.region.trim()) {
35
- throw new Error('OpencodeRuntimeConfig.workerRef.region is required.');
36
- }
37
- if (!ref.allocationId.trim()) {
38
- throw new Error('OpencodeRuntimeConfig.workerRef.allocationId is required.');
39
- }
40
- if (!ref.workerServiceToken.trim()) {
41
- throw new Error('OpencodeRuntimeConfig.workerRef.workerServiceToken is required.');
42
- }
43
- if (!ref.workerControlPassword.trim()) {
44
- throw new Error('OpencodeRuntimeConfig.workerRef.workerControlPassword is required.');
45
- }
46
- const caller = config.caller;
47
- if (!caller.orgId.trim()) {
48
- throw new Error('OpencodeRuntimeConfig.caller.orgId is required.');
49
- }
50
- if (!caller.projectId.trim()) {
51
- throw new Error('OpencodeRuntimeConfig.caller.projectId is required.');
52
- }
53
- if (!caller.authToken.trim()) {
54
- throw new Error('OpencodeRuntimeConfig.caller.authToken is required.');
55
- }
56
- }
@@ -1,344 +0,0 @@
1
- // ═══════════════════════════════════════════════════════════════════════════
2
- // ── OpencodeWorkerRuntime — v1 WorkerRuntime impl ──
3
- //
4
- // A thin adapter that wraps the existing workspace-proxy control plane +
5
- // opencode SSE stream behind the kernel `WorkerRuntime` interface.
6
- //
7
- // This impl deliberately does NOT import from `biomes/workspace-proxy/api/workspace-proxy/` or
8
- // `apps/opencode-pool-api/` — the wire format is the contract. The same
9
- // HTTP endpoints exposed by workspace-proxy are called by this driver
10
- // from outside the pod (the orchestrator runs separately).
11
- // ═══════════════════════════════════════════════════════════════════════════
12
-
13
- import {
14
- HookKind,
15
- WorkerRuntimeKind,
16
- type BuildBundleInput,
17
- type BuildPodSpecInput,
18
- type SessionBundle,
19
- type WorkerRuntime,
20
- type WorkerRuntimeCapabilityBits,
21
- } from '@xemahq/kernel-contracts/worker-runtime';
22
- import type { AbstractMount } from '@xemahq/kernel-contracts/workspace-storage';
23
-
24
- import {
25
- buildOpencodeSessionBundle,
26
- decodeOpencodeSessionBundle,
27
- type OpencodeSessionBundlePayload,
28
- } from './session-bundle';
29
- import type { OpencodeRuntimeConfig, WorkerRef } from './worker-ref';
30
-
31
- /**
32
- * Wire connector kind a session must have bound in scope before the
33
- * orchestrator may select this runtime. Surfaced as a string at the
34
- * kernel boundary (see `WorkerRuntime.connectorKindRequired` —
35
- * `packages/kernel/worker-runtime-contracts/src/lib/runtime.ts:84`).
36
- *
37
- * `'opencode-default'` is the LLM-gateway-backed connector that ships
38
- * by default in every org, and the only one today. A future connector bound to
39
- * a different runtime would expose its own kind here and select that runtime.
40
- */
41
- export const OPENCODE_CONNECTOR_KIND = 'opencode-default';
42
-
43
- /**
44
- * Upper bound for a single `PUT /control/session-bundle` apply (F2). Chosen
45
- * comfortably above a legitimate cold apply — workspace-proxy materializes
46
- * (~sub-second) then restarts opencode with a 30s health-wait ceiling, so a
47
- * correct apply completes in well under a minute even on a loaded node — and
48
- * far below the ~256s OS TCP read timeout that a stuck/black-holed handler
49
- * would otherwise cost. Keeps a wedged apply from stalling the whole allocation
50
- * for minutes.
51
- */
52
- const APPLY_SESSION_BUNDLE_TIMEOUT_MS = 120_000;
53
-
54
- const OPENCODE_CAPABILITIES: WorkerRuntimeCapabilityBits = {
55
- hotMcp: true,
56
- hotModelSwap: true,
57
- sessionFork: true,
58
- subAgentCrud: true,
59
- hooks: [
60
- HookKind.ToolExecuteBefore,
61
- HookKind.ToolExecuteAfter,
62
- HookKind.SessionIdle,
63
- HookKind.SessionStart,
64
- ],
65
- previewSupervisor: true,
66
- gatewayAttributedCalls: true,
67
- llmEndpointOverride: true,
68
- };
69
-
70
- /**
71
- * Shape the K8s scheduler in `biomes/workload-runtime/api/workload-runtime-api/src/scheduler/k8s/`
72
- * narrows when `buildPodSpec` returns. The orchestrator stays
73
- * runtime-agnostic; the scheduler is the only consumer that interprets
74
- * these fields. Mirrors `WorkloadContainerSpecDto` keys.
75
- */
76
- export interface OpencodePodSpec {
77
- readonly runtimeKind: WorkerRuntimeKind;
78
- readonly image: string;
79
- readonly env: Readonly<Record<string, string>>;
80
- readonly mounts: readonly AbstractMount[];
81
- readonly region?: string;
82
- /**
83
- * Translated scheduler-side hint. For k8s this becomes
84
- * `nodeAffinity` on `topology.kubernetes.io/region`; for Docker it
85
- * becomes a container label. The scheduler picks the right shape.
86
- */
87
- readonly nodeAffinity?: {
88
- readonly key: string;
89
- readonly values: readonly string[];
90
- };
91
- readonly requestedCapabilities: BuildPodSpecInput['capabilities'];
92
- }
93
-
94
- /**
95
- * The opencode-runtime adapter. ONE instance per (allocation, worker)
96
- * — the worker identity is bound at construction so the kernel methods
97
- * stay parameter-free per the contract.
98
- */
99
- export class OpencodeWorkerRuntime implements WorkerRuntime {
100
- readonly kind = WorkerRuntimeKind.Opencode;
101
- readonly capabilities = OPENCODE_CAPABILITIES;
102
- readonly connectorKindRequired = OPENCODE_CONNECTOR_KIND;
103
- readonly autoCommitHookSupported = true;
104
-
105
- private workerRef: WorkerRef;
106
- private caller: OpencodeRuntimeConfig['caller'];
107
-
108
- constructor(config: OpencodeRuntimeConfig) {
109
- this.workerRef = config.workerRef;
110
- this.caller = config.caller;
111
- }
112
-
113
- /**
114
- * Translate the schedule-agnostic input to the OpencodePodSpec shape
115
- * the K8s/Docker schedulers consume. The kernel contract types the
116
- * return as `unknown` so the scheduler boundary is the only place that
117
- * narrows. No I/O — pure transform.
118
- */
119
- buildPodSpec(input: BuildPodSpecInput): Promise<OpencodePodSpec> {
120
- const spec: OpencodePodSpec = {
121
- runtimeKind: this.kind,
122
- image: input.image,
123
- env: input.env,
124
- mounts: input.mounts,
125
- ...(input.region !== undefined ? { region: input.region } : {}),
126
- ...(input.region !== undefined
127
- ? {
128
- nodeAffinity: {
129
- key: 'topology.kubernetes.io/region',
130
- values: [input.region],
131
- },
132
- }
133
- : {}),
134
- requestedCapabilities: input.capabilities,
135
- };
136
- return Promise.resolve(spec);
137
- }
138
-
139
- /**
140
- * Compose a SessionBundle. The fingerprint short-circuits the
141
- * workspace-proxy apply call on the next `applySessionBundle`.
142
- */
143
- async buildSessionBundle(input: BuildBundleInput): Promise<SessionBundle> {
144
- const { bundle } = buildOpencodeSessionBundle(
145
- input,
146
- this.workerRef.allocationId,
147
- this.caller,
148
- this.workerRef.bindingEpoch,
149
- // Per-allocation worker service JWT minted by the orchestrator — the
150
- // sole owner of this credential. Delivered on the orchestrator's
151
- // initial applySessionBundle so `/run/xema/service-token` exists before
152
- // working-file arming; the proxy never clears it on later re-applies.
153
- this.workerRef.workerServiceToken,
154
- );
155
- return bundle;
156
- }
157
-
158
- /**
159
- * Push a built bundle to the live worker.
160
- *
161
- * Endpoint: `PUT <controlPlaneUrl>/control/session-bundle`
162
- * Source-of-truth at `biomes/workspace-proxy/api/workspace-proxy/src/handlers/control-plane.ts:377`.
163
- *
164
- * Response contract:
165
- * - 200 `{ unchanged: true, ... }` ← fingerprint matched; no work done.
166
- * - 200 `{ unchanged: false, applied: true, ... }` ← re-materialized.
167
- * - 4xx/5xx ← failure; this method throws.
168
- */
169
- async applySessionBundle(bundle: SessionBundle): Promise<void> {
170
- const payload = decodeOpencodeSessionBundle(bundle);
171
- const url = joinUrl(
172
- this.workerRef.controlPlaneUrl,
173
- '/control/session-bundle',
174
- );
175
- // F2 — bound the apply. workspace-proxy's `/control/session-bundle` handler
176
- // is internally bounded (materialize + a 30s opencode-restart health wait +
177
- // token injection), so a correct apply completes in well under a minute even
178
- // on a cold node. Without an explicit signal a *stuck* handler (e.g. the
179
- // worker pod deleted mid-apply, or a restart storm preempting the restart)
180
- // black-holes the connection until the OS TCP read timeout (~256s), so the
181
- // orchestrator burns ~4 minutes per doomed attempt before it can compensate
182
- // and retry. An `AbortSignal.timeout` well above a legitimate cold apply but
183
- // far below the kernel ceiling converts that into a fast, deterministic
184
- // failure the saga can act on immediately. This is a bounded external-call
185
- // timeout (allowed by the Constitution), NOT a sleep to mask a race.
186
- let response: Response;
187
- try {
188
- response = await fetch(url, {
189
- method: 'PUT',
190
- headers: this.authHeaders(),
191
- body: JSON.stringify(payload),
192
- signal: AbortSignal.timeout(APPLY_SESSION_BUNDLE_TIMEOUT_MS),
193
- });
194
- } catch (err) {
195
- // `AbortSignal.timeout` aborts with a `TimeoutError` DOMException; some
196
- // runtimes surface the abort as `AbortError`. The only abort source here
197
- // is our own timeout, so treat either as the bounded-apply timeout.
198
- if (
199
- err instanceof DOMException &&
200
- (err.name === 'TimeoutError' || err.name === 'AbortError')
201
- ) {
202
- throw new Error(
203
- `applySessionBundle timed out after ${APPLY_SESSION_BUNDLE_TIMEOUT_MS}ms ` +
204
- `waiting on ${url} — the worker control plane accepted the connection ` +
205
- 'but never responded (stuck handler / reaped pod). Failing fast so the ' +
206
- 'allocation saga can compensate and retry.',
207
- );
208
- }
209
- throw err;
210
- }
211
- if (!response.ok) {
212
- const body = await safeReadText(response);
213
- throw new Error(
214
- `applySessionBundle failed: HTTP ${response.status} from ${url} — ${body}`,
215
- );
216
- }
217
- const parsed = (await response.json()) as ApplyBundleResponse;
218
- assertApplyResponseOk(parsed, payload);
219
- }
220
-
221
- /**
222
- * Pause the worker.
223
- *
224
- * Endpoint: `POST <controlPlaneUrl>/control/pause` — the wire shape is
225
- * `200 { paused: true, state: { ... } }` (source at
226
- * `biomes/workspace-proxy/api/workspace-proxy/src/handlers/control-pause-resume.ts`). The
227
- * runtime carries the current bundle fingerprint + allocationId so
228
- * workspace-proxy can stamp them on the pause marker; resume can
229
- * then short-circuit if the bundle is unchanged.
230
- */
231
- async pause(): Promise<void> {
232
- const url = joinUrl(this.workerRef.controlPlaneUrl, '/control/pause');
233
- const body: ControlPauseBody = {
234
- allocationId: this.workerRef.allocationId,
235
- };
236
- const response = await fetch(url, {
237
- method: 'POST',
238
- headers: this.authHeaders(),
239
- body: JSON.stringify(body),
240
- });
241
- if (!response.ok) {
242
- const text = await safeReadText(response);
243
- throw new Error(
244
- `OpencodeWorkerRuntime.pause failed: HTTP ${response.status} from ${url} — ${text}`,
245
- );
246
- }
247
- }
248
-
249
- /**
250
- * Resume a previously-paused worker.
251
- *
252
- * Endpoint: `POST <controlPlaneUrl>/control/resume` — the wire shape is
253
- * `200 { resumed: true, state: { ... } }`. A 409
254
- * `PAUSE_RESUME_FINGERPRINT_MISMATCH` from workspace-proxy means the
255
- * caller must re-apply the bundle (`applySessionBundle`) before
256
- * retrying — surfaced as a typed error here so the orchestrator can
257
- * branch.
258
- */
259
- async resume(): Promise<void> {
260
- const url = joinUrl(this.workerRef.controlPlaneUrl, '/control/resume');
261
- const response = await fetch(url, {
262
- method: 'POST',
263
- headers: this.authHeaders(),
264
- body: JSON.stringify({}),
265
- });
266
- if (!response.ok) {
267
- const text = await safeReadText(response);
268
- throw new Error(
269
- `OpencodeWorkerRuntime.resume failed: HTTP ${response.status} from ${url} — ${text}`,
270
- );
271
- }
272
- }
273
-
274
- private authHeaders(): Record<string, string> {
275
- // workspace-proxy guards every control/session route with HTTP Basic
276
- // auth (`opencode:<OPENCODE_SERVER_PASSWORD>`); a Bearer token is
277
- // rejected (scheme must be `Basic`). The worker's egress JWT
278
- // (workerServiceToken) is a DIFFERENT credential delivered into the pod
279
- // separately — never the inbound control credential.
280
- const encoded = Buffer.from(
281
- `opencode:${this.workerRef.workerControlPassword}`,
282
- ).toString('base64');
283
- return {
284
- Authorization: `Basic ${encoded}`,
285
- 'Content-Type': 'application/json',
286
- };
287
- }
288
- }
289
-
290
- /**
291
- * Body shape for `POST /control/pause`. The fingerprint slot is left
292
- * undefined here because the runtime adapter doesn't track the last
293
- * applied bundle fingerprint — the orchestrator stamps it via
294
- * `applySessionBundle` and workspace-proxy persists it in the marker.
295
- */
296
- interface ControlPauseBody {
297
- readonly allocationId: string;
298
- readonly fingerprint?: string;
299
- }
300
-
301
- interface ApplyBundleResponse {
302
- readonly unchanged?: boolean;
303
- readonly applied?: boolean;
304
- readonly state?: {
305
- readonly allocationId?: string | null;
306
- readonly setupAt?: string | null;
307
- };
308
- readonly error?: string;
309
- readonly status?: string;
310
- }
311
-
312
- function assertApplyResponseOk(
313
- parsed: ApplyBundleResponse,
314
- payload: OpencodeSessionBundlePayload,
315
- ): void {
316
- if (parsed.unchanged === true) return;
317
- if (parsed.applied === true) {
318
- const stateAllocationId = parsed.state?.allocationId ?? null;
319
- if (stateAllocationId !== payload.allocationId) {
320
- throw new Error(
321
- `applySessionBundle returned a state allocationId (${String(stateAllocationId)}) that disagrees with the requested allocationId (${payload.allocationId}). The worker rebound to another session — fail fast.`,
322
- );
323
- }
324
- return;
325
- }
326
- throw new Error(
327
- `applySessionBundle: workspace-proxy returned an unexpected shape — ${JSON.stringify(parsed)}`,
328
- );
329
- }
330
-
331
- async function safeReadText(response: Response): Promise<string> {
332
- try {
333
- return await response.text();
334
- } catch {
335
- return '<unreadable response body>';
336
- }
337
- }
338
-
339
- function joinUrl(base: string, path: string): string {
340
- const trimmedBase = base.endsWith('/') ? base.slice(0, -1) : base;
341
- const prefixedPath = path.startsWith('/') ? path : `/${path}`;
342
- return `${trimmedBase}${prefixedPath}`;
343
- }
344
-
@@ -1,232 +0,0 @@
1
- // ═══════════════════════════════════════════════════════════════════════════
2
- // ── Opencode session-bundle encoding ──
3
- //
4
- // Translates the kernel-shape `BuildBundleInput` into the on-the-wire
5
- // payload that workspace-proxy's `PUT /control/session-bundle` handler
6
- // expects (defined at `biomes/workspace-proxy/api/workspace-proxy/src/handlers/control-plane.ts:816`
7
- // — `interface BundlePayload`).
8
- //
9
- // `staticFingerprint` is the reusable runtime-template compatibility
10
- // identity. `bindingRevision` / `bindingEpoch` identify the tenant/session
11
- // state applied after a compatible worker is claimed.
12
- // ═══════════════════════════════════════════════════════════════════════════
13
-
14
- import { createHash } from 'node:crypto';
15
-
16
- import type {
17
- BuildBundleInput,
18
- SessionBundle,
19
- } from '@xemahq/kernel-contracts/worker-runtime';
20
-
21
- import type { BundleCallerContext } from './worker-ref';
22
-
23
- /**
24
- * Sub-agent entry as the workspace-proxy bundle handler expects it.
25
- * Workspace-proxy validates this shape at runtime; mismatched fields
26
- * fail with HTTP 400. Keep field names byte-for-byte aligned with the
27
- * `BundlePayload.subAgents` parser.
28
- */
29
- export interface OpencodeBundleSubAgent {
30
- readonly slug: string;
31
- readonly alias?: string;
32
- readonly stageKey?: string;
33
- }
34
-
35
- /**
36
- * Fully-formed JSON body for `PUT /control/session-bundle`. Mirrors
37
- * `biomes/workspace-proxy/api/workspace-proxy/src/handlers/control-plane.ts:816` (`BundlePayload`).
38
- */
39
- export interface OpencodeSessionBundlePayload {
40
- readonly allocationId: string;
41
- readonly caller: BundleCallerContext;
42
- readonly opencodeConfigBase64: string;
43
- readonly primaryAgent: { readonly slug: string };
44
- readonly subAgents: readonly OpencodeBundleSubAgent[];
45
- readonly modelOverride?: string;
46
- readonly mcpConfig?: string;
47
- /**
48
- * Per-allocation worker service JWT. workspace-proxy persists it to
49
- * `/run/xema/service-token`, where the in-worker `xema_*` tools + the KB
50
- * working-file adapter read it to authenticate their callbacks. The
51
- * orchestrator is the sole minter/owner and delivers it on its initial
52
- * `applySessionBundle`; the proxy writes-when-present and NEVER clears it,
53
- * so later agent-session-api re-applies (which omit it) leave the token
54
- * intact. Deliberately EXCLUDED from both hashes: raw credentials must
55
- * never enter compatibility or binding identities.
56
- */
57
- readonly serviceToken?: string;
58
- readonly staticFingerprint: string;
59
- readonly bindingRevision: string;
60
- readonly bindingEpoch: number;
61
- }
62
-
63
- /**
64
- * Stable canonical JSON encoder. Keys are sorted alphabetically at every
65
- * object level; undefined values are stripped. The output is the input
66
- * to the SHA-256 fingerprint and MUST be deterministic.
67
- */
68
- function canonicalize(value: unknown): unknown {
69
- if (value === null || typeof value !== 'object') return value;
70
- if (Array.isArray(value)) return value.map((v) => canonicalize(v));
71
- const obj = value as Record<string, unknown>;
72
- const out: Record<string, unknown> = {};
73
- for (const key of Object.keys(obj).sort()) {
74
- const v = obj[key];
75
- if (v === undefined) continue;
76
- out[key] = canonicalize(v);
77
- }
78
- return out;
79
- }
80
-
81
- /**
82
- * Compute the static compatibility fingerprint. It MUST be stable across
83
- * structurally-identical inputs and MUST change when ANY of:
84
- * - primary agent slug,
85
- * - sub-agent set (slug + alias + stageKey),
86
- * - opencode config bytes,
87
- * - MCP config blob,
88
- * differs.
89
- *
90
- * Per-invocation model selection is binding state and is intentionally
91
- * excluded from this runtime-template identity.
92
- */
93
- export function computeStaticBundleFingerprint(
94
- input: BuildBundleInput,
95
- ): string {
96
- const sortedSubAgents = [...input.subAgents].sort();
97
- const canonical = canonicalize({
98
- primaryAgent: input.primaryAgent,
99
- subAgents: sortedSubAgents,
100
- // Static key stays `opencodeConfigBase64` to match workspace-proxy's
101
- // wire-side fingerprint; only the kernel input field was renamed.
102
- opencodeConfigBase64: input.runtimeConfigBase64,
103
- mcpConfig: input.mcpConfig,
104
- });
105
- const hash = createHash('sha256');
106
- hash.update(JSON.stringify(canonical));
107
- return hash.digest('hex');
108
- }
109
-
110
- /**
111
- * Hash only non-secret tenant/session identifiers. Actor and service tokens
112
- * are deliberately absent so credential rotation cannot leak into hashes.
113
- */
114
- export function computeSessionBindingRevision(
115
- allocationId: string,
116
- caller: BundleCallerContext,
117
- ): string {
118
- return createHash('sha256')
119
- .update(
120
- JSON.stringify(
121
- canonicalize({
122
- allocationId,
123
- orgId: caller.orgId,
124
- projectId: caller.projectId,
125
- sessionId: caller.sessionId,
126
- }),
127
- ),
128
- )
129
- .digest('hex');
130
- }
131
-
132
- /**
133
- * Build the wire payload + opaque `SessionBundle` the kernel contract
134
- * exposes. The payload itself is base64-encoded so `SessionBundle` stays
135
- * an opaque blob at the kernel boundary (per the contract docs at
136
- * `packages/kernel/worker-runtime-contracts/src/lib/runtime.ts:66`).
137
- */
138
- export function buildOpencodeSessionBundle(
139
- input: BuildBundleInput,
140
- allocationId: string,
141
- caller: BundleCallerContext,
142
- bindingEpoch: number,
143
- workerServiceToken: string | undefined,
144
- ): {
145
- readonly payload: OpencodeSessionBundlePayload;
146
- readonly bundle: SessionBundle;
147
- } {
148
- if (!input.primaryAgent.trim()) {
149
- throw new Error('BuildBundleInput.primaryAgent must be a non-empty slug.');
150
- }
151
- if (!allocationId.trim()) {
152
- throw new Error('allocationId must be a non-empty string.');
153
- }
154
- if (!Number.isSafeInteger(bindingEpoch) || bindingEpoch < 1) {
155
- throw new Error('bindingEpoch must be a positive safe integer.');
156
- }
157
- const opencodeConfigBase64 = input.runtimeConfigBase64 ?? '';
158
- if (!opencodeConfigBase64) {
159
- throw new Error(
160
- 'BuildBundleInput.runtimeConfigBase64 is required by the opencode runtime — the workspace-proxy bundle handler treats an empty config as a 400.',
161
- );
162
- }
163
- const staticFingerprint = computeStaticBundleFingerprint(input);
164
- const bindingRevision = computeSessionBindingRevision(allocationId, caller);
165
-
166
- const subAgents: readonly OpencodeBundleSubAgent[] = input.subAgents.map(
167
- (slug) => ({ slug }),
168
- );
169
-
170
- // Build the payload, stripping undefined optionals so canonical JSON
171
- // diff stays stable and the workspace-proxy parser does not error on
172
- // an explicit `undefined`. The intermediate `Mutable<>` shape lets us
173
- // add only the optional fields that are actually set, which keeps
174
- // `exactOptionalPropertyTypes: true` happy.
175
- const payload: OpencodeSessionBundlePayload = {
176
- allocationId,
177
- caller,
178
- opencodeConfigBase64,
179
- primaryAgent: { slug: input.primaryAgent },
180
- subAgents,
181
- staticFingerprint,
182
- bindingRevision,
183
- bindingEpoch,
184
- ...(input.modelOverride === undefined
185
- ? {}
186
- : { modelOverride: input.modelOverride }),
187
- ...(input.mcpConfig === undefined ? {} : { mcpConfig: input.mcpConfig }),
188
- // Credential — carried on the payload but NOT in either hash, so a token
189
- // refresh never churns compatibility or binding identity. Omitted when
190
- // empty so the proxy's write-when-present path
191
- // is a no-op rather than clobbering an existing token with "".
192
- ...(workerServiceToken ? { serviceToken: workerServiceToken } : {}),
193
- };
194
-
195
- const payloadJson = JSON.stringify(payload);
196
- const payloadBase64 = Buffer.from(payloadJson, 'utf8').toString('base64');
197
- return {
198
- payload,
199
- bundle: { fingerprint: staticFingerprint, payloadBase64 },
200
- };
201
- }
202
-
203
- /**
204
- * Decode a kernel `SessionBundle` back into the opencode-specific payload.
205
- * Used by `applySessionBundle` so the runtime stays stateless between
206
- * `buildSessionBundle` and `applySessionBundle` (the kernel contract
207
- * does not pass the input through, only the opaque bundle).
208
- */
209
- export function decodeOpencodeSessionBundle(
210
- bundle: SessionBundle,
211
- ): OpencodeSessionBundlePayload {
212
- let parsed: unknown;
213
- try {
214
- const json = Buffer.from(bundle.payloadBase64, 'base64').toString('utf8');
215
- parsed = JSON.parse(json);
216
- } catch (err) {
217
- const msg = err instanceof Error ? err.message : String(err);
218
- throw new Error(
219
- `SessionBundle.payloadBase64 is not a valid base64-encoded JSON object: ${msg}`,
220
- );
221
- }
222
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
223
- throw new Error('SessionBundle payload must decode to a JSON object.');
224
- }
225
- const obj = parsed as { readonly staticFingerprint?: unknown };
226
- if (obj.staticFingerprint !== bundle.fingerprint) {
227
- throw new Error(
228
- `SessionBundle.fingerprint (${bundle.fingerprint}) disagrees with the encoded payload.staticFingerprint (${String(obj.staticFingerprint)}). Refusing to apply a tampered bundle.`,
229
- );
230
- }
231
- return parsed as OpencodeSessionBundlePayload;
232
- }
@@ -1,86 +0,0 @@
1
- // ═══════════════════════════════════════════════════════════════════════════
2
- // ── Opencode runtime — worker reference & runtime config ──
3
- //
4
- // The kernel `WorkerRuntime` contract intentionally hides the target-worker
5
- // identity inside the driver instance. Each opencode runtime is constructed
6
- // for ONE allocated worker pod; the `WorkerRef` is that identity. This
7
- // shape MAY graduate to `@xemahq/kernel-contracts/worker-runtime` once a second
8
- // runtime impl confirms the fields it actually needs.
9
- // ═══════════════════════════════════════════════════════════════════════════
10
-
11
- /**
12
- * Identifies a single live worker pod for an opencode runtime instance.
13
- * Constructed by the orchestrator after `workload-runtime-api`
14
- * schedule + ready callbacks resolve.
15
- */
16
- export interface WorkerRef {
17
- /** Stable id assigned by `workload-runtime-api` at schedule time. */
18
- readonly workerId: string;
19
- /**
20
- * Base URL of the workspace-proxy control plane running INSIDE the pod
21
- * (cluster-internal). e.g. `https://workspace-proxy.<ns>.svc.cluster.local:<port>`.
22
- * The opencode runtime never talks to opencode directly — every wire call
23
- * lands on workspace-proxy, which proxies into the in-pod opencode server.
24
- */
25
- readonly controlPlaneUrl: string;
26
- /** Region the worker is scheduled in; surfaced for observability. */
27
- readonly region: string;
28
- /**
29
- * Allocation id minted by the orchestrator. Included on every
30
- * `/control/session-bundle` PUT so workspace-proxy can detect a
31
- * re-bind to a different session and refuse stale bundles.
32
- */
33
- readonly allocationId: string;
34
- /**
35
- * Monotonic generation of the allocation binding on this worker. Fresh
36
- * workers start at 1; warm-prime reserves epoch 0. A worker reused for a
37
- * different allocation must receive a strictly greater epoch.
38
- */
39
- readonly bindingEpoch: number;
40
- /**
41
- * Worker-service JWT (Keycloak) — the worker's OWN egress identity,
42
- * delivered into the pod via `PUT /control/service-token` and presented
43
- * by the worker on its OUTBOUND calls. It is NOT the credential for
44
- * inbound control-plane auth (see `workerControlPassword`). Refreshing
45
- * it before expiry is the orchestrator's job; that path is NOT exposed
46
- * by this runtime driver.
47
- */
48
- readonly workerServiceToken: string;
49
- /**
50
- * Per-allocation control-plane password. workspace-proxy guards every
51
- * `/control/*` + `/session/*` route with HTTP Basic auth
52
- * (`opencode:<password>`) seeded from its `OPENCODE_SERVER_PASSWORD`
53
- * env, which the orchestrator generated for this allocation. The runtime
54
- * presents it as the Basic credential on every inbound call — a JWT in a
55
- * `Bearer` header is rejected by the proxy (scheme must be `Basic`).
56
- */
57
- readonly workerControlPassword: string;
58
- }
59
-
60
- /**
61
- * Per-runtime-instance configuration the factory accepts.
62
- */
63
- export interface OpencodeRuntimeConfig {
64
- readonly workerRef: WorkerRef;
65
- /**
66
- * Optional caller context written into the session-bundle PUT body.
67
- * The workspace-proxy bundle handler refuses calls without these
68
- * three identifiers, so callers MUST provide them at runtime
69
- * construction (see `BundlePayload.caller` at
70
- * `biomes/workspace-proxy/api/workspace-proxy/src/handlers/control-plane.ts:818`).
71
- */
72
- readonly caller: BundleCallerContext;
73
- }
74
-
75
- /**
76
- * Authentication + scope context the bundle handler stamps onto the
77
- * worker. `authToken` is the END-USER (turn-initiator) actor JWT — the
78
- * service token in `WorkerRef` is presented in the HTTP Authorization
79
- * header, the actor JWT travels in the JSON body.
80
- */
81
- export interface BundleCallerContext {
82
- readonly orgId: string;
83
- readonly projectId: string;
84
- readonly sessionId?: string;
85
- readonly authToken: string;
86
- }