@memberjunction/ai-agent-harness 0.0.0 → 6.1.0-edge.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +192 -27
- package/dist/HarnessAgentBase.d.ts +254 -0
- package/dist/HarnessAgentBase.d.ts.map +1 -0
- package/dist/HarnessAgentBase.js +813 -0
- package/dist/HarnessAgentBase.js.map +1 -0
- package/dist/HarnessAgentType.d.ts +39 -0
- package/dist/HarnessAgentType.d.ts.map +1 -0
- package/dist/HarnessAgentType.js +50 -0
- package/dist/HarnessAgentType.js.map +1 -0
- package/dist/adapters/BaseCliHarnessAdapter.d.ts +93 -0
- package/dist/adapters/BaseCliHarnessAdapter.d.ts.map +1 -0
- package/dist/adapters/BaseCliHarnessAdapter.js +184 -0
- package/dist/adapters/BaseCliHarnessAdapter.js.map +1 -0
- package/dist/adapters/BaseHarnessAdapter.d.ts +114 -0
- package/dist/adapters/BaseHarnessAdapter.d.ts.map +1 -0
- package/dist/adapters/BaseHarnessAdapter.js +86 -0
- package/dist/adapters/BaseHarnessAdapter.js.map +1 -0
- package/dist/adapters/ClaudeCodeCliAdapter.d.ts +104 -0
- package/dist/adapters/ClaudeCodeCliAdapter.d.ts.map +1 -0
- package/dist/adapters/ClaudeCodeCliAdapter.js +268 -0
- package/dist/adapters/ClaudeCodeCliAdapter.js.map +1 -0
- package/dist/adapters/CodexAdapter.d.ts +27 -0
- package/dist/adapters/CodexAdapter.d.ts.map +1 -0
- package/dist/adapters/CodexAdapter.js +117 -0
- package/dist/adapters/CodexAdapter.js.map +1 -0
- package/dist/adapters/GeminiCliAdapter.d.ts +25 -0
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -0
- package/dist/adapters/GeminiCliAdapter.js +98 -0
- package/dist/adapters/GeminiCliAdapter.js.map +1 -0
- package/dist/adapters/OpenCodeAdapter.d.ts +23 -0
- package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -0
- package/dist/adapters/OpenCodeAdapter.js +104 -0
- package/dist/adapters/OpenCodeAdapter.js.map +1 -0
- package/dist/adapters/PiAdapter.d.ts +73 -0
- package/dist/adapters/PiAdapter.d.ts.map +1 -0
- package/dist/adapters/PiAdapter.js +237 -0
- package/dist/adapters/PiAdapter.js.map +1 -0
- package/dist/adapters/StdioJsonAdapter.d.ts +43 -0
- package/dist/adapters/StdioJsonAdapter.d.ts.map +1 -0
- package/dist/adapters/StdioJsonAdapter.js +109 -0
- package/dist/adapters/StdioJsonAdapter.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox/ChildProcessExecutor.d.ts +41 -0
- package/dist/sandbox/ChildProcessExecutor.d.ts.map +1 -0
- package/dist/sandbox/ChildProcessExecutor.js +86 -0
- package/dist/sandbox/ChildProcessExecutor.js.map +1 -0
- package/dist/sandbox/DockerSandboxProvider.d.ts +64 -0
- package/dist/sandbox/DockerSandboxProvider.d.ts.map +1 -0
- package/dist/sandbox/DockerSandboxProvider.js +176 -0
- package/dist/sandbox/DockerSandboxProvider.js.map +1 -0
- package/dist/sandbox/ISandboxProvider.d.ts +71 -0
- package/dist/sandbox/ISandboxProvider.d.ts.map +1 -0
- package/dist/sandbox/ISandboxProvider.js +2 -0
- package/dist/sandbox/ISandboxProvider.js.map +1 -0
- package/dist/sandbox/LocalDirectorySandboxProvider.d.ts +37 -0
- package/dist/sandbox/LocalDirectorySandboxProvider.d.ts.map +1 -0
- package/dist/sandbox/LocalDirectorySandboxProvider.js +75 -0
- package/dist/sandbox/LocalDirectorySandboxProvider.js.map +1 -0
- package/dist/sandbox/SandboxExecutor.d.ts +47 -0
- package/dist/sandbox/SandboxExecutor.d.ts.map +1 -0
- package/dist/sandbox/SandboxExecutor.js +2 -0
- package/dist/sandbox/SandboxExecutor.js.map +1 -0
- package/dist/types.d.ts +180 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +35 -8
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
8
|
+
import { BaseCliHarnessAdapter } from './BaseCliHarnessAdapter.js';
|
|
9
|
+
import { BaseHarnessAdapter } from './BaseHarnessAdapter.js';
|
|
10
|
+
/**
|
|
11
|
+
* Drives ANY command that speaks a documented newline-delimited JSON event contract.
|
|
12
|
+
*
|
|
13
|
+
* This is the escape hatch that keeps "adding a harness must not require core changes" true in the
|
|
14
|
+
* strong sense: a harness MJ has never heard of works with zero MJ code, provided its CLI emits
|
|
15
|
+
* lines matching the vocabulary below.
|
|
16
|
+
*
|
|
17
|
+
* ## The contract
|
|
18
|
+
*
|
|
19
|
+
* Each stdout line is a JSON object with a `type` field:
|
|
20
|
+
*
|
|
21
|
+
* | `type` | Other fields | Meaning |
|
|
22
|
+
* |--------------|---------------------------------------|------------------------------------------|
|
|
23
|
+
* | `text` | `text` | Narration; streamed, never persisted |
|
|
24
|
+
* | `activity` | `description` | In-sandbox activity, for live view only |
|
|
25
|
+
* | `permission` | `id`, `description`, `command?` | Requests approval; becomes a HITL row |
|
|
26
|
+
* | `usage` | `input_tokens`, `output_tokens`, `cost_usd?` | Turn usage; drives cost guardrails |
|
|
27
|
+
* | `complete` | `text` | Turn ended; `text` carries the envelope |
|
|
28
|
+
* | `error` | `message` | Turn failed |
|
|
29
|
+
*
|
|
30
|
+
* Unrecognised lines are ignored, so a harness may interleave its own diagnostics freely.
|
|
31
|
+
*
|
|
32
|
+
* Capabilities default to the conservative end — an unknown harness is assumed NOT to resume
|
|
33
|
+
* sessions or honour permission hooks, so the runtime emulates continuity rather than silently
|
|
34
|
+
* losing context. Override via `AIAgentHarness.CapabilitySettings` when the harness does better.
|
|
35
|
+
*/
|
|
36
|
+
let StdioJsonAdapter = class StdioJsonAdapter extends BaseCliHarnessAdapter {
|
|
37
|
+
constructor() {
|
|
38
|
+
super(...arguments);
|
|
39
|
+
this.executable = 'harness';
|
|
40
|
+
this.extraArgs = [];
|
|
41
|
+
this.declaredCapabilities = null;
|
|
42
|
+
}
|
|
43
|
+
get ExecutablePath() {
|
|
44
|
+
return this.executable;
|
|
45
|
+
}
|
|
46
|
+
/** @inheritdoc */
|
|
47
|
+
get Capabilities() {
|
|
48
|
+
return (this.declaredCapabilities ?? {
|
|
49
|
+
SessionResume: false,
|
|
50
|
+
StructuredOutput: false,
|
|
51
|
+
UsageReporting: true,
|
|
52
|
+
// FALSE by definition: this is the generic escape hatch for a harness with no
|
|
53
|
+
// first-class adapter, so there is no known flag vocabulary to translate a policy
|
|
54
|
+
// into. A subclass that knows its CLI should override ApplyPermissionPolicy and
|
|
55
|
+
// report true.
|
|
56
|
+
PermissionPolicy: false,
|
|
57
|
+
PermissionHooks: false,
|
|
58
|
+
McpClient: false,
|
|
59
|
+
WorkspaceScoping: true,
|
|
60
|
+
ModelSelection: false,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
BuildTurnArgs(input, _isFirstTurn) {
|
|
64
|
+
return [...this.extraArgs, input];
|
|
65
|
+
}
|
|
66
|
+
MapEvent(raw) {
|
|
67
|
+
switch (this.readString(raw, 'type')) {
|
|
68
|
+
case 'text':
|
|
69
|
+
return { Type: 'assistant-text', Text: this.readString(raw, 'text') ?? '' };
|
|
70
|
+
case 'activity':
|
|
71
|
+
return { Type: 'sandbox-activity', Description: this.readString(raw, 'description') ?? 'activity' };
|
|
72
|
+
case 'permission':
|
|
73
|
+
return {
|
|
74
|
+
Type: 'permission-request',
|
|
75
|
+
RequestId: this.readString(raw, 'id') ?? '',
|
|
76
|
+
Description: this.readString(raw, 'description') ?? 'permission requested',
|
|
77
|
+
Command: this.readString(raw, 'command'),
|
|
78
|
+
};
|
|
79
|
+
case 'usage':
|
|
80
|
+
return {
|
|
81
|
+
Type: 'usage',
|
|
82
|
+
InputTokens: this.readNumber(raw, 'input_tokens') ?? 0,
|
|
83
|
+
OutputTokens: this.readNumber(raw, 'output_tokens') ?? 0,
|
|
84
|
+
CostUsd: this.readNumber(raw, 'cost_usd'),
|
|
85
|
+
};
|
|
86
|
+
case 'complete':
|
|
87
|
+
return { Type: 'turn-complete', RawText: this.readString(raw, 'text') ?? '' };
|
|
88
|
+
case 'error':
|
|
89
|
+
return { Type: 'session-error', Error: this.readString(raw, 'message') ?? 'harness reported an error' };
|
|
90
|
+
default:
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Configures the command, extra argv and declared capabilities from `AIAgentHarness` metadata. */
|
|
95
|
+
Configure(executable, extraArgs, capabilities) {
|
|
96
|
+
this.executable = executable;
|
|
97
|
+
this.extraArgs = extraArgs;
|
|
98
|
+
this.declaredCapabilities = capabilities;
|
|
99
|
+
}
|
|
100
|
+
/** Points the adapter at a specific binary, from `AIAgentHarness.ExecutablePath`. */
|
|
101
|
+
SetExecutable(path) {
|
|
102
|
+
this.executable = path;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
StdioJsonAdapter = __decorate([
|
|
106
|
+
RegisterClass(BaseHarnessAdapter, 'StdioJsonAdapter')
|
|
107
|
+
], StdioJsonAdapter);
|
|
108
|
+
export { StdioJsonAdapter };
|
|
109
|
+
//# sourceMappingURL=StdioJsonAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StdioJsonAdapter.js","sourceRoot":"","sources":["../../src/adapters/StdioJsonAdapter.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAsB,MAAM,4BAA4B,CAAC;AACvF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEI,IAAM,gBAAgB,GAAtB,MAAM,gBAAiB,SAAQ,qBAAqB;IAApD;;QACO,eAAU,GAAG,SAAS,CAAC;QACvB,cAAS,GAAa,EAAE,CAAC;QACzB,yBAAoB,GAA+B,IAAI,CAAC;IAsEtE,CAAC;IApEG,IAAc,cAAc;QACxB,OAAO,IAAI,CAAC,UAAU,CAAC;IAC3B,CAAC;IAED,kBAAkB;IAClB,IAAW,YAAY;QACnB,OAAO,CACH,IAAI,CAAC,oBAAoB,IAAI;YACzB,aAAa,EAAE,KAAK;YACpB,gBAAgB,EAAE,KAAK;YACvB,cAAc,EAAE,IAAI;YACpB,8EAA8E;YAC9E,kFAAkF;YAClF,gFAAgF;YAChF,eAAe;YACf,gBAAgB,EAAE,KAAK;YACvB,eAAe,EAAE,KAAK;YACtB,SAAS,EAAE,KAAK;YAChB,gBAAgB,EAAE,IAAI;YACtB,cAAc,EAAE,KAAK;SACxB,CACJ,CAAC;IACN,CAAC;IAES,aAAa,CAAC,KAAa,EAAE,YAAqB;QACxD,OAAO,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACtC,CAAC;IAES,QAAQ,CAAC,GAAuB;QACtC,QAAQ,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC;YACnC,KAAK,MAAM;gBACP,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YAChF,KAAK,UAAU;gBACX,OAAO,EAAE,IAAI,EAAE,kBAAkB,EAAE,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,aAAa,CAAC,IAAI,UAAU,EAAE,CAAC;YACxG,KAAK,YAAY;gBACb,OAAO;oBACH,IAAI,EAAE,oBAAoB;oBAC1B,SAAS,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE;oBAC3C,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,aAAa,CAAC,IAAI,sBAAsB;oBAC1E,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC;iBAC3C,CAAC;YACN,KAAK,OAAO;gBACR,OAAO;oBACH,IAAI,EAAE,OAAO;oBACb,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,cAAc,CAAC,IAAI,CAAC;oBACtD,YAAY,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,eAAe,CAAC,IAAI,CAAC;oBACxD,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC;iBAC5C,CAAC;YACN,KAAK,UAAU;gBACX,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YAClF,KAAK,OAAO;gBACR,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,2BAA2B,EAAE,CAAC;YAC5G;gBACI,OAAO,IAAI,CAAC;QACpB,CAAC;IACL,CAAC;IAED,mGAAmG;IAC5F,SAAS,CAAC,UAAkB,EAAE,SAAmB,EAAE,YAAwC;QAC9F,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,oBAAoB,GAAG,YAAY,CAAC;IAC7C,CAAC;IAED,qFAAqF;IAC9E,aAAa,CAAC,IAAY;QAC7B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;IAC3B,CAAC;CACJ,CAAA;AAzEY,gBAAgB;IAD5B,aAAa,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;GACzC,gBAAgB,CAyE5B"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './HarnessAgentType.js';
|
|
3
|
+
export * from './HarnessAgentBase.js';
|
|
4
|
+
export * from './adapters/BaseHarnessAdapter.js';
|
|
5
|
+
export * from './adapters/BaseCliHarnessAdapter.js';
|
|
6
|
+
export * from './adapters/StdioJsonAdapter.js';
|
|
7
|
+
export * from './adapters/ClaudeCodeCliAdapter.js';
|
|
8
|
+
export * from './adapters/CodexAdapter.js';
|
|
9
|
+
export * from './adapters/OpenCodeAdapter.js';
|
|
10
|
+
export * from './adapters/GeminiCliAdapter.js';
|
|
11
|
+
export * from './adapters/PiAdapter.js';
|
|
12
|
+
export * from './sandbox/SandboxExecutor.js';
|
|
13
|
+
export * from './sandbox/ISandboxProvider.js';
|
|
14
|
+
export * from './sandbox/ChildProcessExecutor.js';
|
|
15
|
+
export * from './sandbox/DockerSandboxProvider.js';
|
|
16
|
+
export * from './sandbox/LocalDirectorySandboxProvider.js';
|
|
17
|
+
/**
|
|
18
|
+
* Tree-shaking guard.
|
|
19
|
+
*
|
|
20
|
+
* The adapters register themselves with ClassFactory via `@RegisterClass` as a side effect of being
|
|
21
|
+
* loaded. A bundler that sees no direct import of a module is free to drop it, which would leave
|
|
22
|
+
* `AIAgentHarness.DriverClass` resolving to nothing at runtime — a failure that only appears in a
|
|
23
|
+
* built artifact, never in dev. Calling this from a consumer's startup path keeps them reachable.
|
|
24
|
+
*/
|
|
25
|
+
export declare function LoadAgentHarnessAdapters(): void;
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kCAAkC,CAAC;AACjD,cAAc,qCAAqC,CAAC;AACpD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oCAAoC,CAAC;AACnD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,4CAA4C,CAAC;AAE3D;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './HarnessAgentType.js';
|
|
3
|
+
export * from './HarnessAgentBase.js';
|
|
4
|
+
export * from './adapters/BaseHarnessAdapter.js';
|
|
5
|
+
export * from './adapters/BaseCliHarnessAdapter.js';
|
|
6
|
+
export * from './adapters/StdioJsonAdapter.js';
|
|
7
|
+
export * from './adapters/ClaudeCodeCliAdapter.js';
|
|
8
|
+
export * from './adapters/CodexAdapter.js';
|
|
9
|
+
export * from './adapters/OpenCodeAdapter.js';
|
|
10
|
+
export * from './adapters/GeminiCliAdapter.js';
|
|
11
|
+
export * from './adapters/PiAdapter.js';
|
|
12
|
+
export * from './sandbox/SandboxExecutor.js';
|
|
13
|
+
export * from './sandbox/ISandboxProvider.js';
|
|
14
|
+
export * from './sandbox/ChildProcessExecutor.js';
|
|
15
|
+
export * from './sandbox/DockerSandboxProvider.js';
|
|
16
|
+
export * from './sandbox/LocalDirectorySandboxProvider.js';
|
|
17
|
+
/**
|
|
18
|
+
* Tree-shaking guard.
|
|
19
|
+
*
|
|
20
|
+
* The adapters register themselves with ClassFactory via `@RegisterClass` as a side effect of being
|
|
21
|
+
* loaded. A bundler that sees no direct import of a module is free to drop it, which would leave
|
|
22
|
+
* `AIAgentHarness.DriverClass` resolving to nothing at runtime — a failure that only appears in a
|
|
23
|
+
* built artifact, never in dev. Calling this from a consumer's startup path keeps them reachable.
|
|
24
|
+
*/
|
|
25
|
+
export function LoadAgentHarnessAdapters() {
|
|
26
|
+
// Intentionally empty — the imports above are the point.
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kCAAkC,CAAC;AACjD,cAAc,qCAAqC,CAAC;AACpD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oCAAoC,CAAC;AACnD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,4CAA4C,CAAC;AAE3D;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB;IACpC,yDAAyD;AAC7D,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
|
+
import { HarnessProcess, HarnessProcessSpec, SandboxExecutor } from './SandboxExecutor.js';
|
|
3
|
+
/**
|
|
4
|
+
* Turns a spawned child process into the backend-neutral {@link HarnessProcess} shape.
|
|
5
|
+
*
|
|
6
|
+
* Shared by every executor that ultimately runs a local binary — the local provider spawns the
|
|
7
|
+
* harness itself, and the Docker provider spawns `docker exec`. Only the argv differs, so the
|
|
8
|
+
* process plumbing lives here once rather than in each provider.
|
|
9
|
+
*/
|
|
10
|
+
export declare function wrapChildProcess(child: ChildProcessWithoutNullStreams): HarnessProcess;
|
|
11
|
+
/**
|
|
12
|
+
* Runs harness processes directly on the MJAPI host.
|
|
13
|
+
*
|
|
14
|
+
* Provides no isolation whatsoever — see {@link LocalDirectorySandboxProvider} for what that means
|
|
15
|
+
* and why it is a development-only posture.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ChildProcessExecutor implements SandboxExecutor {
|
|
18
|
+
private readonly defaultWorkingDirectory;
|
|
19
|
+
constructor(defaultWorkingDirectory: string);
|
|
20
|
+
/**
|
|
21
|
+
* The host variables a locally-spawned harness inherits, before granted credentials are layered
|
|
22
|
+
* on top.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately an ALLOWLIST, not the full process environment: passing everything through would
|
|
25
|
+
* hand the harness whatever credentials the MJAPI process happens to hold, which is exactly the
|
|
26
|
+
* over-granting the credential model exists to prevent.
|
|
27
|
+
*
|
|
28
|
+
* `HOME` is on the list for a specific reason. Local CLI harnesses keep their own login state
|
|
29
|
+
* under the user's home directory (Claude Code in `~/.claude`), so a developer who has already
|
|
30
|
+
* authenticated their CLI can run a harness agent with no credential row and no API key at all —
|
|
31
|
+
* the "true local" mode. Without HOME the harness cannot find its session and reports "Not
|
|
32
|
+
* logged in", which reads as a broken integration rather than a stripped variable.
|
|
33
|
+
*
|
|
34
|
+
* This applies ONLY to local execution. The Docker executor passes just the granted environment,
|
|
35
|
+
* because a container has no business inheriting the host developer's identity.
|
|
36
|
+
*/
|
|
37
|
+
private baseEnvironment;
|
|
38
|
+
/** @inheritdoc */
|
|
39
|
+
Run(spec: HarnessProcessSpec): HarnessProcess;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=ChildProcessExecutor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChildProcessExecutor.d.ts","sourceRoot":"","sources":["../../src/sandbox/ChildProcessExecutor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAS,MAAM,oBAAoB,CAAC;AAG3E,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE3F;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,8BAA8B,GAAG,cAAc,CAoBtF;AAUD;;;;;GAKG;AACH,qBAAa,oBAAqB,YAAW,eAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,uBAAuB;gBAAvB,uBAAuB,EAAE,MAAM;IAEnE;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,eAAe;IAYvB,kBAAkB;IACX,GAAG,CAAC,IAAI,EAAE,kBAAkB,GAAG,cAAc;CAQvD"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
/**
|
|
4
|
+
* Turns a spawned child process into the backend-neutral {@link HarnessProcess} shape.
|
|
5
|
+
*
|
|
6
|
+
* Shared by every executor that ultimately runs a local binary — the local provider spawns the
|
|
7
|
+
* harness itself, and the Docker provider spawns `docker exec`. Only the argv differs, so the
|
|
8
|
+
* process plumbing lives here once rather than in each provider.
|
|
9
|
+
*/
|
|
10
|
+
export function wrapChildProcess(child) {
|
|
11
|
+
return {
|
|
12
|
+
Stdout: readLines(child.stdout),
|
|
13
|
+
Stderr: readLines(child.stderr),
|
|
14
|
+
ExitCode: new Promise((resolve) => {
|
|
15
|
+
if (child.exitCode !== null) {
|
|
16
|
+
resolve(child.exitCode);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
child.once('close', (code) => resolve(code));
|
|
20
|
+
// A spawn failure (ENOENT for a missing binary) emits 'error' and never 'close', so
|
|
21
|
+
// without this the ExitCode promise would hang forever and take the turn with it.
|
|
22
|
+
child.once('error', () => resolve(null));
|
|
23
|
+
}),
|
|
24
|
+
Kill: () => {
|
|
25
|
+
if (child.exitCode === null && !child.killed) {
|
|
26
|
+
child.kill();
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Frames a stream into complete lines. */
|
|
32
|
+
async function* readLines(stream) {
|
|
33
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
34
|
+
for await (const line of lines) {
|
|
35
|
+
yield line;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runs harness processes directly on the MJAPI host.
|
|
40
|
+
*
|
|
41
|
+
* Provides no isolation whatsoever — see {@link LocalDirectorySandboxProvider} for what that means
|
|
42
|
+
* and why it is a development-only posture.
|
|
43
|
+
*/
|
|
44
|
+
export class ChildProcessExecutor {
|
|
45
|
+
constructor(defaultWorkingDirectory) {
|
|
46
|
+
this.defaultWorkingDirectory = defaultWorkingDirectory;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The host variables a locally-spawned harness inherits, before granted credentials are layered
|
|
50
|
+
* on top.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately an ALLOWLIST, not the full process environment: passing everything through would
|
|
53
|
+
* hand the harness whatever credentials the MJAPI process happens to hold, which is exactly the
|
|
54
|
+
* over-granting the credential model exists to prevent.
|
|
55
|
+
*
|
|
56
|
+
* `HOME` is on the list for a specific reason. Local CLI harnesses keep their own login state
|
|
57
|
+
* under the user's home directory (Claude Code in `~/.claude`), so a developer who has already
|
|
58
|
+
* authenticated their CLI can run a harness agent with no credential row and no API key at all —
|
|
59
|
+
* the "true local" mode. Without HOME the harness cannot find its session and reports "Not
|
|
60
|
+
* logged in", which reads as a broken integration rather than a stripped variable.
|
|
61
|
+
*
|
|
62
|
+
* This applies ONLY to local execution. The Docker executor passes just the granted environment,
|
|
63
|
+
* because a container has no business inheriting the host developer's identity.
|
|
64
|
+
*/
|
|
65
|
+
baseEnvironment() {
|
|
66
|
+
const allowed = ['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'TMPDIR', 'LANG', 'TERM'];
|
|
67
|
+
const env = {};
|
|
68
|
+
for (const key of allowed) {
|
|
69
|
+
const value = process.env[key];
|
|
70
|
+
if (value !== undefined) {
|
|
71
|
+
env[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return env;
|
|
75
|
+
}
|
|
76
|
+
/** @inheritdoc */
|
|
77
|
+
Run(spec) {
|
|
78
|
+
const child = spawn(spec.Command, spec.Args, {
|
|
79
|
+
cwd: spec.WorkingDirectory ?? this.defaultWorkingDirectory,
|
|
80
|
+
env: { ...this.baseEnvironment(), ...spec.Environment },
|
|
81
|
+
signal: spec.CancellationToken,
|
|
82
|
+
});
|
|
83
|
+
return wrapChildProcess(child);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=ChildProcessExecutor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChildProcessExecutor.js","sourceRoot":"","sources":["../../src/sandbox/ChildProcessExecutor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3E,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAqC;IAClE,OAAO;QACH,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;QAC/B,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;QAC/B,QAAQ,EAAE,IAAI,OAAO,CAAgB,CAAC,OAAO,EAAE,EAAE;YAC7C,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;gBAC1B,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBACxB,OAAO;YACX,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7C,oFAAoF;YACpF,kFAAkF;YAClF,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7C,CAAC,CAAC;QACF,IAAI,EAAE,GAAG,EAAE;YACP,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;gBAC3C,KAAK,CAAC,IAAI,EAAE,CAAC;YACjB,CAAC;QACL,CAAC;KACJ,CAAC;AACN,CAAC;AAED,2CAA2C;AAC3C,KAAK,SAAS,CAAC,CAAC,SAAS,CAAC,MAAgB;IACtC,MAAM,KAAK,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;IACtE,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC7B,MAAM,IAAI,CAAC;IACf,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,oBAAoB;IAC7B,YAAoC,uBAA+B;QAA/B,4BAAuB,GAAvB,uBAAuB,CAAQ;IAAG,CAAC;IAEvE;;;;;;;;;;;;;;;;OAgBG;IACK,eAAe;QACnB,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACvF,MAAM,GAAG,GAA2B,EAAE,CAAC;QACvC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YACxB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACtB,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACrB,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED,kBAAkB;IACX,GAAG,CAAC,IAAwB;QAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE;YACzC,GAAG,EAAE,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,uBAAuB;YAC1D,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC,eAAe,EAAE,EAAE,GAAG,IAAI,CAAC,WAAW,EAAE;YACvD,MAAM,EAAE,IAAI,CAAC,iBAAiB;SACjC,CAAC,CAAC;QACH,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;CACJ"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ISandboxProvider, SandboxConfig, SandboxHandle, WorkspaceKey } from './ISandboxProvider.js';
|
|
2
|
+
import { HarnessProcess, HarnessProcessSpec, SandboxExecutor } from './SandboxExecutor.js';
|
|
3
|
+
/**
|
|
4
|
+
* Runs each harness turn inside a per-run container.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this exists
|
|
7
|
+
*
|
|
8
|
+
* {@link LocalDirectorySandboxProvider} scopes a directory but does not contain the process: the
|
|
9
|
+
* harness runs on the MJAPI host with that host's network reach and cloud credentials. For a feature
|
|
10
|
+
* whose entire purpose is executing an autonomous agent's shell commands, that is the wrong blast
|
|
11
|
+
* radius anywhere but a developer's laptop.
|
|
12
|
+
*
|
|
13
|
+
* Here the harness runs in a container with the workspace bind-mounted, so a file write outside the
|
|
14
|
+
* workspace hits the container's filesystem and dies with it, and `networkPolicy` is enforced by
|
|
15
|
+
* Docker rather than merely documented.
|
|
16
|
+
*
|
|
17
|
+
* ## Container per RUN, exec per TURN
|
|
18
|
+
*
|
|
19
|
+
* The container starts once at {@link Provision} and every turn is a `docker exec` into it. The
|
|
20
|
+
* alternative — `docker run` per turn — would pay container startup on every turn and, worse, lose
|
|
21
|
+
* any in-container state the harness accumulated outside the mounted workspace. A run is the natural
|
|
22
|
+
* lifetime because it is exactly the span over which a harness session is continuous.
|
|
23
|
+
*
|
|
24
|
+
* ## Network policy
|
|
25
|
+
*
|
|
26
|
+
* `none` maps to `--network none`. `mcp-only` and `allowlist` currently map to a bridge network and
|
|
27
|
+
* are NOT yet enforced at the packet level — enforcing them properly needs a per-run network with
|
|
28
|
+
* egress rules, which is the next increment. They are documented here as not-yet-enforced rather
|
|
29
|
+
* than quietly treated as equivalent to `open`, because an operator who believes `mcp-only` is
|
|
30
|
+
* enforced has a false sense of containment, which is worse than knowing the boundary is soft.
|
|
31
|
+
*/
|
|
32
|
+
export declare class DockerSandboxProvider implements ISandboxProvider {
|
|
33
|
+
private readonly hostRootPath;
|
|
34
|
+
private readonly defaultImage;
|
|
35
|
+
private readonly containers;
|
|
36
|
+
constructor(options?: {
|
|
37
|
+
hostRootPath?: string;
|
|
38
|
+
defaultImage?: string;
|
|
39
|
+
});
|
|
40
|
+
/** @inheritdoc */
|
|
41
|
+
Provision(key: WorkspaceKey, config: SandboxConfig): Promise<SandboxHandle>;
|
|
42
|
+
/** @inheritdoc */
|
|
43
|
+
Finalize(handle: SandboxHandle, _outcome: 'success' | 'failure' | 'cancelled'): Promise<void>;
|
|
44
|
+
/** Maps the declared network policy onto docker flags, honestly. */
|
|
45
|
+
private buildNetworkArgs;
|
|
46
|
+
/** Runs a docker CLI command and resolves with its trimmed stdout. */
|
|
47
|
+
private runDockerCommand;
|
|
48
|
+
/** Same shape as the local provider, so a workspace is recognisable across both. */
|
|
49
|
+
private buildRelativePath;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Runs harness processes via `docker exec` into an already-running per-run container.
|
|
53
|
+
*
|
|
54
|
+
* Environment is passed with repeated `--env` flags rather than baked into the container at start,
|
|
55
|
+
* so a credential rotated between turns takes effect on the next turn without recreating the
|
|
56
|
+
* sandbox.
|
|
57
|
+
*/
|
|
58
|
+
export declare class DockerExecExecutor implements SandboxExecutor {
|
|
59
|
+
private readonly containerName;
|
|
60
|
+
constructor(containerName: string);
|
|
61
|
+
/** @inheritdoc */
|
|
62
|
+
Run(spec: HarnessProcessSpec): HarnessProcess;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=DockerSandboxProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DockerSandboxProvider.d.ts","sourceRoot":"","sources":["../../src/sandbox/DockerSandboxProvider.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrG,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAO3F;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,qBAAsB,YAAW,gBAAgB;IAC1D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;gBAErC,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE;IAK7E,kBAAkB;IACL,SAAS,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAsCxF,kBAAkB;IACL,QAAQ,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB1G,oEAAoE;IACpE,OAAO,CAAC,gBAAgB;IAexB,sEAAsE;IACtE,OAAO,CAAC,gBAAgB;IAkBxB,oFAAoF;IACpF,OAAO,CAAC,iBAAiB;CAU5B;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,YAAW,eAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAb,aAAa,EAAE,MAAM;IAEzD,kBAAkB;IACX,GAAG,CAAC,IAAI,EAAE,kBAAkB,GAAG,cAAc;CAavD"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { LogError, LogStatus } from '@memberjunction/core';
|
|
6
|
+
import { wrapChildProcess } from './ChildProcessExecutor.js';
|
|
7
|
+
/** Path the host workspace is mounted at inside the container. */
|
|
8
|
+
const CONTAINER_WORKSPACE = '/workspace';
|
|
9
|
+
/**
|
|
10
|
+
* Runs each harness turn inside a per-run container.
|
|
11
|
+
*
|
|
12
|
+
* ## Why this exists
|
|
13
|
+
*
|
|
14
|
+
* {@link LocalDirectorySandboxProvider} scopes a directory but does not contain the process: the
|
|
15
|
+
* harness runs on the MJAPI host with that host's network reach and cloud credentials. For a feature
|
|
16
|
+
* whose entire purpose is executing an autonomous agent's shell commands, that is the wrong blast
|
|
17
|
+
* radius anywhere but a developer's laptop.
|
|
18
|
+
*
|
|
19
|
+
* Here the harness runs in a container with the workspace bind-mounted, so a file write outside the
|
|
20
|
+
* workspace hits the container's filesystem and dies with it, and `networkPolicy` is enforced by
|
|
21
|
+
* Docker rather than merely documented.
|
|
22
|
+
*
|
|
23
|
+
* ## Container per RUN, exec per TURN
|
|
24
|
+
*
|
|
25
|
+
* The container starts once at {@link Provision} and every turn is a `docker exec` into it. The
|
|
26
|
+
* alternative — `docker run` per turn — would pay container startup on every turn and, worse, lose
|
|
27
|
+
* any in-container state the harness accumulated outside the mounted workspace. A run is the natural
|
|
28
|
+
* lifetime because it is exactly the span over which a harness session is continuous.
|
|
29
|
+
*
|
|
30
|
+
* ## Network policy
|
|
31
|
+
*
|
|
32
|
+
* `none` maps to `--network none`. `mcp-only` and `allowlist` currently map to a bridge network and
|
|
33
|
+
* are NOT yet enforced at the packet level — enforcing them properly needs a per-run network with
|
|
34
|
+
* egress rules, which is the next increment. They are documented here as not-yet-enforced rather
|
|
35
|
+
* than quietly treated as equivalent to `open`, because an operator who believes `mcp-only` is
|
|
36
|
+
* enforced has a false sense of containment, which is worse than knowing the boundary is soft.
|
|
37
|
+
*/
|
|
38
|
+
export class DockerSandboxProvider {
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.containers = new Map();
|
|
41
|
+
this.hostRootPath = options?.hostRootPath ?? join(tmpdir(), 'mj-agent-harness');
|
|
42
|
+
this.defaultImage = options?.defaultImage ?? 'ghcr.io/memberjunction/harness-sandbox:latest';
|
|
43
|
+
}
|
|
44
|
+
/** @inheritdoc */
|
|
45
|
+
async Provision(key, config) {
|
|
46
|
+
const hostPath = join(this.hostRootPath, this.buildRelativePath(key));
|
|
47
|
+
await mkdir(hostPath, { recursive: true });
|
|
48
|
+
const image = config.Image ?? this.defaultImage;
|
|
49
|
+
const containerName = `mj-harness-${key.RunId}`;
|
|
50
|
+
const args = [
|
|
51
|
+
'run',
|
|
52
|
+
'--detach',
|
|
53
|
+
'--rm',
|
|
54
|
+
'--name',
|
|
55
|
+
containerName,
|
|
56
|
+
'--volume',
|
|
57
|
+
`${hostPath}:${CONTAINER_WORKSPACE}`,
|
|
58
|
+
'--workdir',
|
|
59
|
+
CONTAINER_WORKSPACE,
|
|
60
|
+
...this.buildNetworkArgs(config.NetworkPolicy),
|
|
61
|
+
image,
|
|
62
|
+
// Keep the container alive so turns can exec into it; the harness itself is never this
|
|
63
|
+
// process, it is whatever `docker exec` runs.
|
|
64
|
+
'sleep',
|
|
65
|
+
'infinity',
|
|
66
|
+
];
|
|
67
|
+
const containerId = await this.runDockerCommand(args);
|
|
68
|
+
this.containers.set(containerName, containerId);
|
|
69
|
+
LogStatus(`Harness container started: ${containerName} (${image})`);
|
|
70
|
+
return {
|
|
71
|
+
// The path AS THE HARNESS SEES IT — inside the container, not on the host. Anything that
|
|
72
|
+
// tries to open this with `fs` on the MJAPI host is wrong; see SandboxHandle's note.
|
|
73
|
+
WorkspacePath: CONTAINER_WORKSPACE,
|
|
74
|
+
Key: key,
|
|
75
|
+
Ephemeral: key.Scope === 'run',
|
|
76
|
+
Executor: new DockerExecExecutor(containerName),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** @inheritdoc */
|
|
80
|
+
async Finalize(handle, _outcome) {
|
|
81
|
+
const containerName = `mj-harness-${handle.Key.RunId}`;
|
|
82
|
+
try {
|
|
83
|
+
// --rm on the container means stopping it removes it, so this is both stop and cleanup.
|
|
84
|
+
await this.runDockerCommand(['stop', '--time', '5', containerName]);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
// Never rethrow from finalize: it runs on failure and cancellation paths, where throwing
|
|
88
|
+
// would replace the real error with a cleanup error and lose the diagnosis.
|
|
89
|
+
LogError(`Failed to stop harness container ${containerName}: ${describeError(e)}`);
|
|
90
|
+
}
|
|
91
|
+
this.containers.delete(containerName);
|
|
92
|
+
if (handle.Ephemeral) {
|
|
93
|
+
const hostPath = join(this.hostRootPath, this.buildRelativePath(handle.Key));
|
|
94
|
+
try {
|
|
95
|
+
await rm(hostPath, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
LogError(`Failed to remove harness workspace ${hostPath}: ${describeError(e)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Maps the declared network policy onto docker flags, honestly. */
|
|
103
|
+
buildNetworkArgs(policy) {
|
|
104
|
+
switch (policy) {
|
|
105
|
+
case 'none':
|
|
106
|
+
// Fully enforced: no interfaces at all.
|
|
107
|
+
return ['--network', 'none'];
|
|
108
|
+
case 'mcp-only':
|
|
109
|
+
case 'allowlist':
|
|
110
|
+
// NOT yet enforced at the packet level — see the class doc. Deliberately identical
|
|
111
|
+
// to 'open' today rather than pretending otherwise.
|
|
112
|
+
return [];
|
|
113
|
+
case 'open':
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Runs a docker CLI command and resolves with its trimmed stdout. */
|
|
118
|
+
runDockerCommand(args) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const child = spawn('docker', args);
|
|
121
|
+
const out = [];
|
|
122
|
+
const err = [];
|
|
123
|
+
child.stdout.on('data', (c) => out.push(c.toString()));
|
|
124
|
+
child.stderr.on('data', (c) => err.push(c.toString()));
|
|
125
|
+
child.once('error', (e) => reject(e));
|
|
126
|
+
child.once('close', (code) => {
|
|
127
|
+
if (code === 0) {
|
|
128
|
+
resolve(out.join('').trim());
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
reject(new Error(`docker ${args[0]} failed (code ${code}): ${err.join('').trim()}`));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** Same shape as the local provider, so a workspace is recognisable across both. */
|
|
137
|
+
buildRelativePath(key) {
|
|
138
|
+
switch (key.Scope) {
|
|
139
|
+
case 'run':
|
|
140
|
+
return join('run', key.RunId);
|
|
141
|
+
case 'agent':
|
|
142
|
+
return join('agent', key.AgentId);
|
|
143
|
+
case 'agent-user':
|
|
144
|
+
return join('agent-user', key.AgentId, key.UserId ?? 'no-user');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Runs harness processes via `docker exec` into an already-running per-run container.
|
|
150
|
+
*
|
|
151
|
+
* Environment is passed with repeated `--env` flags rather than baked into the container at start,
|
|
152
|
+
* so a credential rotated between turns takes effect on the next turn without recreating the
|
|
153
|
+
* sandbox.
|
|
154
|
+
*/
|
|
155
|
+
export class DockerExecExecutor {
|
|
156
|
+
constructor(containerName) {
|
|
157
|
+
this.containerName = containerName;
|
|
158
|
+
}
|
|
159
|
+
/** @inheritdoc */
|
|
160
|
+
Run(spec) {
|
|
161
|
+
const args = ['exec', '--interactive'];
|
|
162
|
+
for (const [key, value] of Object.entries(spec.Environment)) {
|
|
163
|
+
args.push('--env', `${key}=${value}`);
|
|
164
|
+
}
|
|
165
|
+
if (spec.WorkingDirectory) {
|
|
166
|
+
args.push('--workdir', spec.WorkingDirectory);
|
|
167
|
+
}
|
|
168
|
+
args.push(this.containerName, spec.Command, ...spec.Args);
|
|
169
|
+
const child = spawn('docker', args, { signal: spec.CancellationToken });
|
|
170
|
+
return wrapChildProcess(child);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function describeError(e) {
|
|
174
|
+
return e instanceof Error ? e.message : String(e);
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=DockerSandboxProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DockerSandboxProvider.js","sourceRoot":"","sources":["../../src/sandbox/DockerSandboxProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { HarnessNetworkPolicy, HarnessWorkspaceScope } from '../types.js';
|
|
2
|
+
import { SandboxExecutor } from './SandboxExecutor.js';
|
|
3
|
+
/**
|
|
4
|
+
* Identifies which workspace a run should get, and therefore how long its files live.
|
|
5
|
+
*
|
|
6
|
+
* Scope is the deciding field: `run` means a fresh directory nobody sees again, `agent` a shared
|
|
7
|
+
* workspace across every run of that agent, `agent-user` one per agent per user — the default,
|
|
8
|
+
* because it gives an agent continuity without letting one user's working files leak into another's
|
|
9
|
+
* session.
|
|
10
|
+
*/
|
|
11
|
+
export interface WorkspaceKey {
|
|
12
|
+
Scope: HarnessWorkspaceScope;
|
|
13
|
+
AgentId: string;
|
|
14
|
+
UserId?: string;
|
|
15
|
+
RunId: string;
|
|
16
|
+
}
|
|
17
|
+
/** Runtime knobs a provider honours when provisioning. */
|
|
18
|
+
export interface SandboxConfig {
|
|
19
|
+
NetworkPolicy: HarnessNetworkPolicy;
|
|
20
|
+
/** Container image, for providers that have one. Ignored by the local provider. */
|
|
21
|
+
Image?: string;
|
|
22
|
+
/** How to handle two runs wanting the same durable workspace. */
|
|
23
|
+
Concurrency?: 'queue' | 'fail' | 'fork';
|
|
24
|
+
}
|
|
25
|
+
/** A provisioned workspace, and the means of running commands inside it. */
|
|
26
|
+
export interface SandboxHandle {
|
|
27
|
+
/**
|
|
28
|
+
* Workspace path AS THE HARNESS SEES IT.
|
|
29
|
+
*
|
|
30
|
+
* For the local provider that is a host path. For a container provider it is the path inside the
|
|
31
|
+
* container, which is generally NOT the host path the workspace was created at — the two are
|
|
32
|
+
* connected by a mount the provider set up. Code that passes this to a harness process is
|
|
33
|
+
* correct; code that opens it with `fs` on the MJAPI host is only correct for the local provider,
|
|
34
|
+
* and that distinction is why the field is documented rather than just named.
|
|
35
|
+
*/
|
|
36
|
+
WorkspacePath: string;
|
|
37
|
+
Key: WorkspaceKey;
|
|
38
|
+
/** True when the workspace is discarded on finalize rather than retained for the next run. */
|
|
39
|
+
Ephemeral: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Runs harness processes inside this sandbox.
|
|
42
|
+
*
|
|
43
|
+
* The reason process placement lives on the HANDLE rather than in the adapter: an adapter that
|
|
44
|
+
* called `spawn()` directly would always run on the MJAPI host, which in production means an
|
|
45
|
+
* autonomous agent executing shell commands inside the API container with its network reach and
|
|
46
|
+
* cloud credentials. Routing every process through the provider's executor is what makes
|
|
47
|
+
* `provider: 'docker'` mean something.
|
|
48
|
+
*/
|
|
49
|
+
Executor: SandboxExecutor;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Provisions and finalizes the filesystem a harness runs against.
|
|
53
|
+
*
|
|
54
|
+
* Kept deliberately small — two methods — because the interesting variation between a local
|
|
55
|
+
* directory and a container is not the shape of the API, it is what `NetworkPolicy` can actually
|
|
56
|
+
* enforce. The local provider can only honour it on a best-effort basis; a container provider
|
|
57
|
+
* enforces it for real, which is why `mcp-only` is the recommended production posture and only
|
|
58
|
+
* meaningful there.
|
|
59
|
+
*/
|
|
60
|
+
export interface ISandboxProvider {
|
|
61
|
+
/** Provisions (or reattaches to) the workspace identified by `key`. */
|
|
62
|
+
Provision(key: WorkspaceKey, config: SandboxConfig): Promise<SandboxHandle>;
|
|
63
|
+
/**
|
|
64
|
+
* Releases the workspace.
|
|
65
|
+
*
|
|
66
|
+
* Must be safe to call on every exit path including crash and cancellation, and must not throw —
|
|
67
|
+
* a finalize that throws inside a failure path masks the original error with a cleanup error.
|
|
68
|
+
*/
|
|
69
|
+
Finalize(handle: SandboxHandle, outcome: 'success' | 'failure' | 'cancelled'): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=ISandboxProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ISandboxProvider.d.ts","sourceRoot":"","sources":["../../src/sandbox/ISandboxProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,qBAAqB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC1B,aAAa,EAAE,oBAAoB,CAAC;IACpC,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;CAC3C;AAED,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC1B;;;;;;;;OAQG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,GAAG,EAAE,YAAY,CAAC;IAClB,8FAA8F;IAC9F,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;;;OAQG;IACH,QAAQ,EAAE,eAAe,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC7B,uEAAuE;IACvE,SAAS,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5E;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChG"}
|