@nebutra/agent-runtime 0.2.2 → 2.0.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/README.md +33 -10
- package/dist/{chunk-KCNN4QUQ.js → chunk-5UGIUYJR.js} +4 -4
- package/dist/chunk-7ELA4SWE.js +73 -0
- package/dist/chunk-7ELA4SWE.js.map +1 -0
- package/dist/chunk-VXILZAXK.js +541 -0
- package/dist/chunk-VXILZAXK.js.map +1 -0
- package/dist/command-exec.d.ts +45 -0
- package/dist/command-exec.js +15 -0
- package/dist/command-exec.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +68 -12
- package/dist/index.js.map +1 -1
- package/dist/pulsar.js +2 -2
- package/dist/sandbox-DfcRttXt.d.ts +219 -0
- package/dist/sandbox.d.ts +2 -64
- package/dist/sandbox.js +25 -3
- package/package.json +8 -7
- package/dist/chunk-R5HOSQUW.js +0 -58
- package/dist/chunk-R5HOSQUW.js.map +0 -1
- /package/dist/{chunk-KCNN4QUQ.js.map → chunk-5UGIUYJR.js.map} +0 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
// src/carina-ndjson.ts
|
|
2
|
+
import { createConnection } from "net";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
var defaultCarinaSocketPath = () => join(homedir(), ".carina", "daemon.sock");
|
|
6
|
+
var CODEPLOY_CARINA_SOCKET_PATH = "/var/carina/run/daemon.sock";
|
|
7
|
+
var CODEPLOY_CARINA_WORKSPACE_ROOT = "/var/carina/ws";
|
|
8
|
+
var CODEPLOY_CARINA_STATE_DIR = "/var/carina/state";
|
|
9
|
+
var CODEPLOY_CARINA_BIN_DIR = "/var/carina/bin";
|
|
10
|
+
var CarinaNdjsonError = class extends Error {
|
|
11
|
+
constructor(message, status, code) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = "CarinaNdjsonError";
|
|
16
|
+
}
|
|
17
|
+
status;
|
|
18
|
+
code;
|
|
19
|
+
};
|
|
20
|
+
function createCarinaNdjsonClient(socketPath, callTimeoutMs = 3e4) {
|
|
21
|
+
let socket = null;
|
|
22
|
+
let connecting = null;
|
|
23
|
+
let nextId = 0;
|
|
24
|
+
let buffer = "";
|
|
25
|
+
const pending = /* @__PURE__ */ new Map();
|
|
26
|
+
const failAll = (err) => {
|
|
27
|
+
for (const [id, p] of pending) {
|
|
28
|
+
clearTimeout(p.timer);
|
|
29
|
+
p.reject(err);
|
|
30
|
+
pending.delete(id);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const onData = (chunk) => {
|
|
34
|
+
buffer += chunk;
|
|
35
|
+
for (; ; ) {
|
|
36
|
+
const nl = buffer.indexOf("\n");
|
|
37
|
+
if (nl < 0) break;
|
|
38
|
+
const line = buffer.slice(0, nl).trim();
|
|
39
|
+
buffer = buffer.slice(nl + 1);
|
|
40
|
+
if (!line) continue;
|
|
41
|
+
let msg;
|
|
42
|
+
try {
|
|
43
|
+
msg = JSON.parse(line);
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (msg.id === void 0 || msg.id === null) continue;
|
|
48
|
+
const id = typeof msg.id === "number" ? msg.id : Number(msg.id);
|
|
49
|
+
const p = pending.get(id);
|
|
50
|
+
if (!p) continue;
|
|
51
|
+
pending.delete(id);
|
|
52
|
+
clearTimeout(p.timer);
|
|
53
|
+
if (msg.error) {
|
|
54
|
+
p.reject(
|
|
55
|
+
new CarinaNdjsonError(
|
|
56
|
+
`Carina RPC failed (${msg.error.code}): ${msg.error.message}`,
|
|
57
|
+
502,
|
|
58
|
+
String(msg.error.code)
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
p.resolve(msg.result);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const connect = async () => {
|
|
67
|
+
if (socket && !socket.destroyed) return;
|
|
68
|
+
if (connecting) return connecting;
|
|
69
|
+
connecting = new Promise((resolve, reject) => {
|
|
70
|
+
const s = createConnection(socketPath);
|
|
71
|
+
const onFail = (error) => {
|
|
72
|
+
s.destroy();
|
|
73
|
+
reject(
|
|
74
|
+
new CarinaNdjsonError(
|
|
75
|
+
`cannot reach carina-daemon at ${socketPath}: ${error.message}`,
|
|
76
|
+
503,
|
|
77
|
+
"transport_error"
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
s.once("error", onFail);
|
|
82
|
+
s.once("connect", () => {
|
|
83
|
+
s.off("error", onFail);
|
|
84
|
+
socket = s;
|
|
85
|
+
s.setEncoding("utf8");
|
|
86
|
+
s.on("data", (chunk) => onData(String(chunk)));
|
|
87
|
+
s.on("error", (error) => {
|
|
88
|
+
failAll(
|
|
89
|
+
new CarinaNdjsonError(
|
|
90
|
+
`carina-daemon socket error: ${error.message}`,
|
|
91
|
+
503,
|
|
92
|
+
"transport_error"
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
socket = null;
|
|
96
|
+
});
|
|
97
|
+
s.on("close", () => {
|
|
98
|
+
failAll(new CarinaNdjsonError("carina-daemon connection closed", 503, "transport_error"));
|
|
99
|
+
socket = null;
|
|
100
|
+
});
|
|
101
|
+
resolve();
|
|
102
|
+
});
|
|
103
|
+
}).finally(() => {
|
|
104
|
+
connecting = null;
|
|
105
|
+
});
|
|
106
|
+
return connecting;
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
async call(method, params) {
|
|
110
|
+
await connect();
|
|
111
|
+
const s = socket;
|
|
112
|
+
if (!s || s.destroyed) {
|
|
113
|
+
throw new CarinaNdjsonError("carina-daemon is disconnected", 503, "transport_error");
|
|
114
|
+
}
|
|
115
|
+
const id = ++nextId;
|
|
116
|
+
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
pending.delete(id);
|
|
120
|
+
reject(
|
|
121
|
+
new CarinaNdjsonError(
|
|
122
|
+
`Carina RPC ${method} timed out after ${callTimeoutMs}ms`,
|
|
123
|
+
504,
|
|
124
|
+
"timeout"
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
}, callTimeoutMs);
|
|
128
|
+
pending.set(id, {
|
|
129
|
+
timer,
|
|
130
|
+
resolve: (v) => resolve(v),
|
|
131
|
+
reject
|
|
132
|
+
});
|
|
133
|
+
s.write(payload, (error) => {
|
|
134
|
+
if (!error) return;
|
|
135
|
+
const p = pending.get(id);
|
|
136
|
+
if (!p) return;
|
|
137
|
+
pending.delete(id);
|
|
138
|
+
clearTimeout(p.timer);
|
|
139
|
+
p.reject(
|
|
140
|
+
new CarinaNdjsonError(
|
|
141
|
+
`Carina RPC ${method} write failed: ${error.message}`,
|
|
142
|
+
503,
|
|
143
|
+
"transport_error"
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
close() {
|
|
150
|
+
failAll(new CarinaNdjsonError("carina ndjson client closed", 503, "closed"));
|
|
151
|
+
socket?.destroy();
|
|
152
|
+
socket = null;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/sandbox.ts
|
|
158
|
+
var NoExecutorConfiguredError = class extends Error {
|
|
159
|
+
constructor() {
|
|
160
|
+
super(
|
|
161
|
+
"No ExternalSandbox configured. This runtime never executes untrusted code in-process; delegate to Carina (Track B) via createCarinaSandbox."
|
|
162
|
+
);
|
|
163
|
+
this.name = "NoExecutorConfiguredError";
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
var REFUSING_SANDBOX = {
|
|
167
|
+
async exec() {
|
|
168
|
+
throw new NoExecutorConfiguredError();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
var SandboxDelegationError = class extends Error {
|
|
172
|
+
constructor(message, status, code) {
|
|
173
|
+
super(message);
|
|
174
|
+
this.status = status;
|
|
175
|
+
this.code = code;
|
|
176
|
+
this.name = "SandboxDelegationError";
|
|
177
|
+
}
|
|
178
|
+
status;
|
|
179
|
+
code;
|
|
180
|
+
};
|
|
181
|
+
var CarinaProtocolError = class extends Error {
|
|
182
|
+
constructor(message) {
|
|
183
|
+
super(message);
|
|
184
|
+
this.name = "CarinaProtocolError";
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function createHttpSandbox(baseUrl, fetchImpl = fetch, path = "/api/v1/sandbox/exec") {
|
|
188
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
189
|
+
return {
|
|
190
|
+
async exec(request) {
|
|
191
|
+
const response = await fetchImpl(endpoint, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: { "content-type": "application/json" },
|
|
194
|
+
body: JSON.stringify(request)
|
|
195
|
+
});
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
const detail = await response.text().catch(() => "");
|
|
198
|
+
throw new SandboxDelegationError(
|
|
199
|
+
`Isolator refused execution (${response.status}): ${detail}`,
|
|
200
|
+
response.status
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const body = await response.json();
|
|
204
|
+
if (!body.executedOn) {
|
|
205
|
+
return { ...body, executedOn: "http-sandbox" };
|
|
206
|
+
}
|
|
207
|
+
return body;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
var CARINA_MIN_PROTOCOL_VERSION = 1;
|
|
212
|
+
function extractCarinaSessionId(session) {
|
|
213
|
+
if (!session || typeof session !== "object") {
|
|
214
|
+
throw new CarinaProtocolError("Carina session.create returned non-object Session");
|
|
215
|
+
}
|
|
216
|
+
const rec = session;
|
|
217
|
+
for (const key of ["id", "session_id", "sessionId"]) {
|
|
218
|
+
const v = rec[key];
|
|
219
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
220
|
+
}
|
|
221
|
+
throw new CarinaProtocolError("Carina session.create Session missing id/session_id/sessionId");
|
|
222
|
+
}
|
|
223
|
+
function createCarinaSandbox(options) {
|
|
224
|
+
const {
|
|
225
|
+
baseUrl,
|
|
226
|
+
socketPath,
|
|
227
|
+
token,
|
|
228
|
+
fetchImpl = fetch,
|
|
229
|
+
rpcPath = "",
|
|
230
|
+
minProtocolVersion = CARINA_MIN_PROTOCOL_VERSION,
|
|
231
|
+
skipHello = false,
|
|
232
|
+
executedOn = "carina",
|
|
233
|
+
resolveSessionId,
|
|
234
|
+
clientId = "nebutra-sailor",
|
|
235
|
+
autoApproveOnRequire = false
|
|
236
|
+
} = options;
|
|
237
|
+
const httpBase = baseUrl?.trim();
|
|
238
|
+
const sock = socketPath?.trim();
|
|
239
|
+
if (!httpBase && !sock) {
|
|
240
|
+
throw new CarinaProtocolError(
|
|
241
|
+
"createCarinaSandbox requires baseUrl (HTTP) or socketPath (unix NDJSON)"
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const sessionByThread = /* @__PURE__ */ new Map();
|
|
245
|
+
const endpoint = httpBase ? `${httpBase.replace(/\/$/, "")}${!rpcPath ? "" : rpcPath.startsWith("/") ? rpcPath : `/${rpcPath}`}` : "";
|
|
246
|
+
let helloDone = null;
|
|
247
|
+
let rpcId = 0;
|
|
248
|
+
let ndjson = null;
|
|
249
|
+
function getNdjson() {
|
|
250
|
+
if (!sock) {
|
|
251
|
+
throw new CarinaProtocolError("socket transport requested without socketPath");
|
|
252
|
+
}
|
|
253
|
+
if (!ndjson) ndjson = createCarinaNdjsonClient(sock);
|
|
254
|
+
return ndjson;
|
|
255
|
+
}
|
|
256
|
+
async function rpcCallHttp(method, params) {
|
|
257
|
+
const id = ++rpcId;
|
|
258
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
259
|
+
if (token) {
|
|
260
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
261
|
+
}
|
|
262
|
+
const response = await fetchImpl(endpoint, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers,
|
|
265
|
+
body: JSON.stringify({
|
|
266
|
+
jsonrpc: "2.0",
|
|
267
|
+
id,
|
|
268
|
+
method,
|
|
269
|
+
params
|
|
270
|
+
})
|
|
271
|
+
});
|
|
272
|
+
if (!response.ok) {
|
|
273
|
+
const detail = await response.text().catch(() => "");
|
|
274
|
+
throw new SandboxDelegationError(
|
|
275
|
+
`Carina transport error (${response.status}): ${detail}`,
|
|
276
|
+
response.status,
|
|
277
|
+
"transport_error"
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const body = await response.json();
|
|
281
|
+
if (body.error) {
|
|
282
|
+
throw new SandboxDelegationError(
|
|
283
|
+
`Carina RPC ${method} failed (${body.error.code}): ${body.error.message}`,
|
|
284
|
+
502,
|
|
285
|
+
String(body.error.code)
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (!("result" in body)) {
|
|
289
|
+
throw new CarinaProtocolError(`Carina RPC ${method} returned no result`);
|
|
290
|
+
}
|
|
291
|
+
return body.result;
|
|
292
|
+
}
|
|
293
|
+
async function rpcCall(method, params) {
|
|
294
|
+
if (sock && !httpBase) {
|
|
295
|
+
try {
|
|
296
|
+
return await getNdjson().call(method, params);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (err instanceof CarinaNdjsonError) {
|
|
299
|
+
throw new SandboxDelegationError(err.message, err.status, err.code);
|
|
300
|
+
}
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return rpcCallHttp(method, params);
|
|
305
|
+
}
|
|
306
|
+
async function ensureHello() {
|
|
307
|
+
if (skipHello) return;
|
|
308
|
+
if (!helloDone) {
|
|
309
|
+
helloDone = (async () => {
|
|
310
|
+
const hello = await rpcCall("gateway.hello", {
|
|
311
|
+
protocol_version: minProtocolVersion,
|
|
312
|
+
client_id: clientId
|
|
313
|
+
});
|
|
314
|
+
const version = hello.protocol_version ?? 0;
|
|
315
|
+
if (typeof version !== "number" || version < minProtocolVersion) {
|
|
316
|
+
throw new CarinaProtocolError(
|
|
317
|
+
`Carina protocol_version ${String(hello.protocol_version)} below required ${minProtocolVersion}`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
})().catch((err) => {
|
|
321
|
+
helloDone = null;
|
|
322
|
+
throw err;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
await helloDone;
|
|
326
|
+
}
|
|
327
|
+
const defaultResolveSessionId = (r) => sessionByThread.get(r.threadId) ?? r.threadId;
|
|
328
|
+
const mapSessionId = resolveSessionId ?? defaultResolveSessionId;
|
|
329
|
+
return {
|
|
330
|
+
resolveMappedSessionId(threadId) {
|
|
331
|
+
return sessionByThread.get(threadId);
|
|
332
|
+
},
|
|
333
|
+
async probe() {
|
|
334
|
+
await ensureHello();
|
|
335
|
+
const hello = await rpcCall("gateway.hello", {
|
|
336
|
+
protocol_version: minProtocolVersion,
|
|
337
|
+
client_id: clientId
|
|
338
|
+
});
|
|
339
|
+
return { protocolVersion: hello.protocol_version ?? minProtocolVersion, ok: true };
|
|
340
|
+
},
|
|
341
|
+
async resolveApproval(request) {
|
|
342
|
+
const decisionId = request.decisionId.trim();
|
|
343
|
+
if (!decisionId) {
|
|
344
|
+
throw new CarinaProtocolError("resolveApproval requires decisionId");
|
|
345
|
+
}
|
|
346
|
+
await ensureHello();
|
|
347
|
+
const params = {
|
|
348
|
+
decision_id: decisionId,
|
|
349
|
+
approve: request.approve
|
|
350
|
+
};
|
|
351
|
+
if (request.approver) params.approver = request.approver;
|
|
352
|
+
if (request.scope) params.scope = request.scope;
|
|
353
|
+
return rpcCall("governance.approval.resolve", params);
|
|
354
|
+
},
|
|
355
|
+
async ensureSession(request) {
|
|
356
|
+
const cached = sessionByThread.get(request.threadId);
|
|
357
|
+
if (cached) return cached;
|
|
358
|
+
const root = request.workspaceRoot.trim();
|
|
359
|
+
if (!root) {
|
|
360
|
+
throw new CarinaProtocolError("Carina session.create requires a non-empty workspaceRoot");
|
|
361
|
+
}
|
|
362
|
+
await ensureHello();
|
|
363
|
+
const params = { workspace_root: root };
|
|
364
|
+
if (request.profile) params.profile = request.profile;
|
|
365
|
+
if (request.approvalMode) params.approval_mode = request.approvalMode;
|
|
366
|
+
if (request.tenantId) params.tenant_id = request.tenantId;
|
|
367
|
+
params.correlation_id = request.threadId;
|
|
368
|
+
const session = await rpcCall("session.create", params);
|
|
369
|
+
const sessionId = extractCarinaSessionId(session);
|
|
370
|
+
sessionByThread.set(request.threadId, sessionId);
|
|
371
|
+
return sessionId;
|
|
372
|
+
},
|
|
373
|
+
async exec(request) {
|
|
374
|
+
assertSafePosture(request.capabilityPolicy);
|
|
375
|
+
await ensureHello();
|
|
376
|
+
const sessionId = mapSessionId(request);
|
|
377
|
+
const argv = ["/bin/sh", "-c", request.command];
|
|
378
|
+
let wire = await rpcCall("command.exec", {
|
|
379
|
+
session_id: sessionId,
|
|
380
|
+
argv,
|
|
381
|
+
// Host correlation for audit — ignored if kernel drops unknown fields.
|
|
382
|
+
correlation_id: `${request.tenantId}:${request.threadId}`,
|
|
383
|
+
tenant_id: request.tenantId
|
|
384
|
+
});
|
|
385
|
+
let decision = wire.decision?.decision ?? "denied";
|
|
386
|
+
let decisionId = wire.decision?.decision_id;
|
|
387
|
+
if (decision === "denied") {
|
|
388
|
+
throw new SandboxDelegationError(
|
|
389
|
+
`Carina denied command: ${wire.decision?.reason ?? "denied"}`,
|
|
390
|
+
403,
|
|
391
|
+
"denied"
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (decision === "requires_approval") {
|
|
395
|
+
if (autoApproveOnRequire && decisionId) {
|
|
396
|
+
await rpcCall("governance.approval.resolve", {
|
|
397
|
+
decision_id: decisionId,
|
|
398
|
+
approve: true,
|
|
399
|
+
scope: "once",
|
|
400
|
+
approver: clientId
|
|
401
|
+
});
|
|
402
|
+
wire = await rpcCall("command.exec", {
|
|
403
|
+
session_id: sessionId,
|
|
404
|
+
argv,
|
|
405
|
+
correlation_id: `${request.tenantId}:${request.threadId}`,
|
|
406
|
+
tenant_id: request.tenantId
|
|
407
|
+
});
|
|
408
|
+
const retryDecision = wire.decision?.decision ?? "denied";
|
|
409
|
+
if (retryDecision === "allowed") {
|
|
410
|
+
decision = "allowed";
|
|
411
|
+
decisionId = wire.decision?.decision_id ?? decisionId;
|
|
412
|
+
} else if (retryDecision === "denied") {
|
|
413
|
+
throw new SandboxDelegationError(
|
|
414
|
+
`Carina denied command after auto-approve: ${wire.decision?.reason ?? "denied"}`,
|
|
415
|
+
403,
|
|
416
|
+
"denied"
|
|
417
|
+
);
|
|
418
|
+
} else {
|
|
419
|
+
throw new SandboxDelegationError(
|
|
420
|
+
`Carina still requires approval after auto-approve` + (wire.decision?.decision_id ? ` (decision_id=${wire.decision.decision_id})` : ""),
|
|
421
|
+
409,
|
|
422
|
+
"requires_approval"
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
throw new SandboxDelegationError(
|
|
427
|
+
`Carina requires approval` + (decisionId ? ` (decision_id=${decisionId})` : ""),
|
|
428
|
+
409,
|
|
429
|
+
"requires_approval"
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const cr = wire.result;
|
|
434
|
+
if (!cr) {
|
|
435
|
+
throw new SandboxDelegationError(
|
|
436
|
+
"Carina allowed exec but returned no CommandResult",
|
|
437
|
+
502,
|
|
438
|
+
"missing_result"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const stdout = (cr.stdout ?? []).join("");
|
|
442
|
+
const stderr = (cr.stderr ?? []).join("");
|
|
443
|
+
const aggregatedOutput = stderr.length > 0 ? `${stdout}${stdout ? "\n" : ""}${stderr}` : stdout;
|
|
444
|
+
return {
|
|
445
|
+
exitCode: cr.exit_code ?? (cr.timed_out ? 124 : 0),
|
|
446
|
+
aggregatedOutput,
|
|
447
|
+
executedOn,
|
|
448
|
+
// exactOptionalPropertyTypes: omit key when undefined
|
|
449
|
+
...decisionId !== void 0 ? { decisionId } : {},
|
|
450
|
+
correlationId: `${request.tenantId}:${request.threadId}`
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function assertSafePosture(policy, allowDanger = false) {
|
|
456
|
+
if (policy.kind === "danger_full_access" && !allowDanger) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
"danger_full_access capability policy refused: not permitted for multi-tenant delegation without explicit opt-in."
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function resolveCarinaWorkspaceRoot(tenantId, env = process.env, threadId = "") {
|
|
463
|
+
const mapRaw = env.CARINA_WORKSPACE_MAP?.trim();
|
|
464
|
+
if (mapRaw) {
|
|
465
|
+
try {
|
|
466
|
+
const map = JSON.parse(mapRaw);
|
|
467
|
+
const hit = map[tenantId];
|
|
468
|
+
if (typeof hit === "string" && hit.trim()) return hit.trim();
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const template = env.CARINA_WORKSPACE_TEMPLATE?.trim();
|
|
473
|
+
if (template) {
|
|
474
|
+
return template.replaceAll("{tenantId}", tenantId).replaceAll("{threadId}", threadId).trim();
|
|
475
|
+
}
|
|
476
|
+
const root = env.CARINA_WORKSPACE_ROOT?.trim();
|
|
477
|
+
return root || void 0;
|
|
478
|
+
}
|
|
479
|
+
function isCarinaCodeployEnabled(env = process.env) {
|
|
480
|
+
const v = env.CARINA_CODEPLOY?.trim().toLowerCase();
|
|
481
|
+
if (v === "0" || v === "false" || v === "off") return false;
|
|
482
|
+
if (v === "1" || v === "true" || v === "on") return true;
|
|
483
|
+
if (env.CARINA_JSONRPC_URL?.trim()) return true;
|
|
484
|
+
if (env.CARINA_DAEMON_SOCK?.trim()) return true;
|
|
485
|
+
return v === void 0 || v === "";
|
|
486
|
+
}
|
|
487
|
+
function resolveCarinaSandboxFromEnv(env = process.env, overrides = {}) {
|
|
488
|
+
const auto = env.CARINA_AUTO_APPROVE === "1" || env.CARINA_AUTO_APPROVE === "true";
|
|
489
|
+
const common = {
|
|
490
|
+
...env.CARINA_CLIENT_ID ? { clientId: env.CARINA_CLIENT_ID } : {},
|
|
491
|
+
...auto ? { autoApproveOnRequire: true } : {},
|
|
492
|
+
...overrides
|
|
493
|
+
};
|
|
494
|
+
const baseUrl = env.CARINA_JSONRPC_URL?.trim();
|
|
495
|
+
if (baseUrl) {
|
|
496
|
+
return createCarinaSandbox({
|
|
497
|
+
baseUrl,
|
|
498
|
+
...env.CARINA_JSONRPC_TOKEN ? { token: env.CARINA_JSONRPC_TOKEN } : {},
|
|
499
|
+
...env.CARINA_JSONRPC_PATH ? { rpcPath: env.CARINA_JSONRPC_PATH } : {},
|
|
500
|
+
...common
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (!isCarinaCodeployEnabled(env) && !env.CARINA_DAEMON_SOCK?.trim()) {
|
|
504
|
+
return REFUSING_SANDBOX;
|
|
505
|
+
}
|
|
506
|
+
const sock = env.CARINA_DAEMON_SOCK?.trim() || (env.CARINA_CODEPLOY_HOME === "1" || env.CARINA_CODEPLOY_HOME === "true" ? defaultCarinaSocketPath() : CODEPLOY_CARINA_SOCKET_PATH);
|
|
507
|
+
return createCarinaSandbox({
|
|
508
|
+
socketPath: sock,
|
|
509
|
+
...common
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function resolveCarinaWorkspaceRootWithCodeploy(tenantId, env = process.env, threadId = "") {
|
|
513
|
+
return resolveCarinaWorkspaceRoot(tenantId, env, threadId) ?? (isCarinaCodeployEnabled(env) ? CODEPLOY_CARINA_WORKSPACE_ROOT : void 0);
|
|
514
|
+
}
|
|
515
|
+
function isCarinaSandbox(sandbox) {
|
|
516
|
+
return typeof sandbox.ensureSession === "function" && typeof sandbox.resolveMappedSessionId === "function";
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export {
|
|
520
|
+
defaultCarinaSocketPath,
|
|
521
|
+
CODEPLOY_CARINA_SOCKET_PATH,
|
|
522
|
+
CODEPLOY_CARINA_WORKSPACE_ROOT,
|
|
523
|
+
CODEPLOY_CARINA_STATE_DIR,
|
|
524
|
+
CODEPLOY_CARINA_BIN_DIR,
|
|
525
|
+
CarinaNdjsonError,
|
|
526
|
+
createCarinaNdjsonClient,
|
|
527
|
+
NoExecutorConfiguredError,
|
|
528
|
+
REFUSING_SANDBOX,
|
|
529
|
+
SandboxDelegationError,
|
|
530
|
+
CarinaProtocolError,
|
|
531
|
+
createHttpSandbox,
|
|
532
|
+
CARINA_MIN_PROTOCOL_VERSION,
|
|
533
|
+
createCarinaSandbox,
|
|
534
|
+
assertSafePosture,
|
|
535
|
+
resolveCarinaWorkspaceRoot,
|
|
536
|
+
isCarinaCodeployEnabled,
|
|
537
|
+
resolveCarinaSandboxFromEnv,
|
|
538
|
+
resolveCarinaWorkspaceRootWithCodeploy,
|
|
539
|
+
isCarinaSandbox
|
|
540
|
+
};
|
|
541
|
+
//# sourceMappingURL=chunk-VXILZAXK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/carina-ndjson.ts","../src/sandbox.ts"],"sourcesContent":["/**\n * Carina native transport: newline-delimited JSON-RPC 2.0 over a Unix socket\n * (same wire as @carina/sdk and `carina-daemon -socket`).\n *\n * Node-only — not for edge runtimes without `node:net`.\n */\n\nimport { createConnection, type Socket } from \"node:net\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const defaultCarinaSocketPath = (): string => join(homedir(), \".carina\", \"daemon.sock\");\n\n/** Production co-deploy layout on the Sailor ECS api host. */\nexport const CODEPLOY_CARINA_SOCKET_PATH = \"/var/carina/run/daemon.sock\";\nexport const CODEPLOY_CARINA_WORKSPACE_ROOT = \"/var/carina/ws\";\nexport const CODEPLOY_CARINA_STATE_DIR = \"/var/carina/state\";\nexport const CODEPLOY_CARINA_BIN_DIR = \"/var/carina/bin\";\n\nexport class CarinaNdjsonError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly code?: string,\n ) {\n super(message);\n this.name = \"CarinaNdjsonError\";\n }\n}\n\ntype Pending = {\n resolve: (value: unknown) => void;\n reject: (err: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n};\n\nexport type NdjsonRpcClient = {\n call: <T>(method: string, params: Record<string, unknown>) => Promise<T>;\n close: () => void;\n};\n\n/**\n * Create a reusable NDJSON JSON-RPC client for a Carina unix socket.\n */\nexport function createCarinaNdjsonClient(\n socketPath: string,\n callTimeoutMs = 30_000,\n): NdjsonRpcClient {\n let socket: Socket | null = null;\n let connecting: Promise<void> | null = null;\n let nextId = 0;\n let buffer = \"\";\n const pending = new Map<number, Pending>();\n\n const failAll = (err: Error): void => {\n for (const [id, p] of pending) {\n clearTimeout(p.timer);\n p.reject(err);\n pending.delete(id);\n }\n };\n\n const onData = (chunk: string): void => {\n buffer += chunk;\n for (;;) {\n const nl = buffer.indexOf(\"\\n\");\n if (nl < 0) break;\n const line = buffer.slice(0, nl).trim();\n buffer = buffer.slice(nl + 1);\n if (!line) continue;\n let msg: {\n id?: number | string;\n result?: unknown;\n error?: { code: number; message: string };\n };\n try {\n msg = JSON.parse(line) as typeof msg;\n } catch {\n continue;\n }\n if (msg.id === undefined || msg.id === null) continue;\n const id = typeof msg.id === \"number\" ? msg.id : Number(msg.id);\n const p = pending.get(id);\n if (!p) continue;\n pending.delete(id);\n clearTimeout(p.timer);\n if (msg.error) {\n p.reject(\n new CarinaNdjsonError(\n `Carina RPC failed (${msg.error.code}): ${msg.error.message}`,\n 502,\n String(msg.error.code),\n ),\n );\n continue;\n }\n p.resolve(msg.result);\n }\n };\n\n const connect = async (): Promise<void> => {\n if (socket && !socket.destroyed) return;\n if (connecting) return connecting;\n connecting = new Promise<void>((resolve, reject) => {\n const s = createConnection(socketPath);\n const onFail = (error: Error): void => {\n s.destroy();\n reject(\n new CarinaNdjsonError(\n `cannot reach carina-daemon at ${socketPath}: ${error.message}`,\n 503,\n \"transport_error\",\n ),\n );\n };\n s.once(\"error\", onFail);\n s.once(\"connect\", () => {\n s.off(\"error\", onFail);\n socket = s;\n s.setEncoding(\"utf8\");\n s.on(\"data\", (chunk: string | Buffer) => onData(String(chunk)));\n s.on(\"error\", (error) => {\n failAll(\n new CarinaNdjsonError(\n `carina-daemon socket error: ${error.message}`,\n 503,\n \"transport_error\",\n ),\n );\n socket = null;\n });\n s.on(\"close\", () => {\n failAll(new CarinaNdjsonError(\"carina-daemon connection closed\", 503, \"transport_error\"));\n socket = null;\n });\n resolve();\n });\n }).finally(() => {\n connecting = null;\n });\n return connecting;\n };\n\n return {\n async call<T>(method: string, params: Record<string, unknown>): Promise<T> {\n await connect();\n const s = socket;\n if (!s || s.destroyed) {\n throw new CarinaNdjsonError(\"carina-daemon is disconnected\", 503, \"transport_error\");\n }\n const id = ++nextId;\n const payload = JSON.stringify({ jsonrpc: \"2.0\", id, method, params }) + \"\\n\";\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(id);\n reject(\n new CarinaNdjsonError(\n `Carina RPC ${method} timed out after ${callTimeoutMs}ms`,\n 504,\n \"timeout\",\n ),\n );\n }, callTimeoutMs);\n pending.set(id, {\n timer,\n resolve: (v) => resolve(v as T),\n reject,\n });\n s.write(payload, (error) => {\n if (!error) return;\n const p = pending.get(id);\n if (!p) return;\n pending.delete(id);\n clearTimeout(p.timer);\n p.reject(\n new CarinaNdjsonError(\n `Carina RPC ${method} write failed: ${error.message}`,\n 503,\n \"transport_error\",\n ),\n );\n });\n });\n },\n close(): void {\n failAll(new CarinaNdjsonError(\"carina ndjson client closed\", 503, \"closed\"));\n socket?.destroy();\n socket = null;\n },\n };\n}\n","/**\n * External-sandbox delegation seam (PORT — capability #11).\n *\n * Governance decision (dual-track, ExternalSandbox posture): this multi-tenant\n * web runtime NEVER executes untrusted code itself and introduces no isolation\n * infrastructure. It only:\n * 1. carries the capability policy (see ./policy),\n * 2. delegates execution to an external isolator behind this interface\n * (Track B = **Carina** upstream kernel),\n * 3. records the outcome as a `command_execution` item (see ./model).\n *\n * The upstream OS enforcers (Seatbelt / Landlock / bubblewrap / Windows\n * restricted token) are deliberately NOT ported — they are single-host and\n * out of scope for a multi-tenant web product.\n *\n * @see docs/architecture/2026-08-03-carina-track-b-upstream.md\n */\n\nimport {\n CarinaNdjsonError,\n CODEPLOY_CARINA_SOCKET_PATH,\n CODEPLOY_CARINA_WORKSPACE_ROOT,\n createCarinaNdjsonClient,\n defaultCarinaSocketPath,\n type NdjsonRpcClient,\n} from \"./carina-ndjson\";\nimport type { CapabilityPolicy } from \"./policy\";\n\nexport interface SandboxExecRequest {\n /** Mandatory tenant scope — every delegated exec is tenant-bound. */\n readonly tenantId: string;\n readonly threadId: string;\n readonly command: string;\n /** The capabilities the external isolator must honor. */\n readonly capabilityPolicy: CapabilityPolicy;\n /** Optional inputs the executor should materialize before running. */\n readonly inputs?: Readonly<Record<string, string>>;\n}\n\nexport interface SandboxExecResult {\n readonly exitCode: number;\n readonly aggregatedOutput: string;\n /** Identifier of the isolator that actually ran the command. */\n readonly executedOn: string;\n /** Carina permission decision id when present (audit correlation). */\n readonly decisionId?: string;\n /** Host correlation id sent on the wire (JSON-RPC id or extension). */\n readonly correlationId?: string;\n}\n\n/**\n * The only thing Track A depends on for execution. Implemented by a decoupled\n * isolator over Carina's public RPC catalog; never implemented in-process here.\n */\nexport interface ExternalSandbox {\n exec(request: SandboxExecRequest): Promise<SandboxExecResult>;\n}\n\n/**\n * Fail-closed default. Wiring this in production requires a real\n * {@link ExternalSandbox}; absent one, untrusted code never runs — by design.\n */\nexport class NoExecutorConfiguredError extends Error {\n constructor() {\n super(\n \"No ExternalSandbox configured. This runtime never executes untrusted \" +\n \"code in-process; delegate to Carina (Track B) via createCarinaSandbox.\",\n );\n this.name = \"NoExecutorConfiguredError\";\n }\n}\n\nexport const REFUSING_SANDBOX: ExternalSandbox = {\n async exec(): Promise<SandboxExecResult> {\n throw new NoExecutorConfiguredError();\n },\n};\n\nexport class SandboxDelegationError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly code?: string,\n ) {\n super(message);\n this.name = \"SandboxDelegationError\";\n }\n}\n\nexport class CarinaProtocolError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CarinaProtocolError\";\n }\n}\n\n/**\n * Generic HTTP POST of a Sailor-shaped body. Prefer {@link createCarinaSandbox}\n * for production Track B — Carina owns the kernel and speaks JSON-RPC.\n */\nexport function createHttpSandbox(\n baseUrl: string,\n fetchImpl: typeof fetch = fetch,\n path = \"/api/v1/sandbox/exec\",\n): ExternalSandbox {\n const endpoint = `${baseUrl.replace(/\\/$/, \"\")}${path.startsWith(\"/\") ? path : `/${path}`}`;\n return {\n async exec(request: SandboxExecRequest): Promise<SandboxExecResult> {\n const response = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request),\n });\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new SandboxDelegationError(\n `Isolator refused execution (${response.status}): ${detail}`,\n response.status,\n );\n }\n const body = (await response.json()) as SandboxExecResult;\n if (!body.executedOn) {\n return { ...body, executedOn: \"http-sandbox\" };\n }\n return body;\n },\n };\n}\n\n// ── Carina v0.8.x JSON-RPC mapping (catalog baseline 0.8.1; pin co-deploy binary separately) ─\n\n/** Minimum `protocol_version` we accept from `gateway.hello` (Carina catalog). */\nexport const CARINA_MIN_PROTOCOL_VERSION = 1;\n\ntype JsonRpcSuccess<T> = {\n jsonrpc: \"2.0\";\n id: string | number;\n result: T;\n error?: undefined;\n};\n\ntype JsonRpcFailure = {\n jsonrpc: \"2.0\";\n id: string | number | null;\n error: { code: number; message: string; data?: unknown };\n result?: undefined;\n};\n\ntype JsonRpcResponse<T> = JsonRpcSuccess<T> | JsonRpcFailure;\n\ntype CarinaDecision = {\n decision_id?: string;\n decision?: \"allowed\" | \"denied\" | \"requires_approval\";\n reason?: string;\n};\n\ntype CarinaCommandResult = {\n exit_code?: number;\n duration_ms?: number;\n stdout?: string[];\n stderr?: string[];\n timed_out?: boolean;\n};\n\ntype CarinaExecWire = {\n decision?: CarinaDecision;\n result?: CarinaCommandResult;\n};\n\ntype CarinaHello = {\n protocol_version?: number;\n version?: string;\n features?: unknown;\n methods?: unknown;\n};\n\nexport type CarinaSandboxOptions = {\n /**\n * HTTP JSON-RPC base URL (optional if {@link socketPath} is set).\n * Prefer unix socket co-deploy on the same host as the gateway.\n */\n readonly baseUrl?: string;\n /**\n * Native Carina unix socket path (NDJSON JSON-RPC). Preferred for same-host\n * co-deploy — full command.exec authority (TCP diagnostic is read-limited).\n */\n readonly socketPath?: string;\n /**\n * Gateway token for HTTP transport only. Never a local owner unlock token.\n */\n readonly token?: string;\n readonly fetchImpl?: typeof fetch;\n /**\n * Path under baseUrl for JSON-RPC POST. Default `\"\"` posts to baseUrl itself.\n */\n readonly rpcPath?: string;\n /** Fail closed if hello reports a lower protocol_version. Default {@link CARINA_MIN_PROTOCOL_VERSION}. */\n readonly minProtocolVersion?: number;\n /** Skip gateway.hello (tests only). Production should leave this false. */\n readonly skipHello?: boolean;\n /** Label when the wire body has no node id. */\n readonly executedOn?: string;\n /** Map Sailor thread → Carina `session_id`. Default: `threadId`. */\n readonly resolveSessionId?: (request: SandboxExecRequest) => string;\n /**\n * Optional client metadata sent on hello (discovery only — not an auth grant).\n */\n readonly clientId?: string;\n /**\n * When true, `command.exec` that returns `requires_approval` will call\n * `governance.approval.resolve` (approve=true) once and retry. Default false\n * (fail closed). Operators set via CARINA_AUTO_APPROVE=1 for unattended nodes.\n */\n readonly autoApproveOnRequire?: boolean;\n};\n\nexport type CarinaEnsureSessionRequest = {\n /** Sailor thread id — used as cache key and default correlation. */\n readonly threadId: string;\n /** Absolute workspace path on the Carina host (catalog-required). */\n readonly workspaceRoot: string;\n readonly tenantId?: string;\n readonly profile?: string;\n /** Carina session approval_mode when supported by the daemon. */\n readonly approvalMode?: string;\n};\n\n/**\n * Track-B handle: {@link ExternalSandbox} plus host session lifecycle.\n * `command.exec` requires a Carina session; call {@link CarinaSandbox.ensureSession}\n * before the first exec for a thread (or let {@link registerCommandExecTool} do it).\n */\nexport type CarinaApprovalResolveRequest = {\n readonly decisionId: string;\n readonly approve: boolean;\n readonly approver?: string;\n readonly scope?: \"once\" | \"session\" | \"project\";\n};\n\nexport interface CarinaSandbox extends ExternalSandbox {\n /**\n * Create (or reuse cached) Carina session for a Sailor thread.\n * Maps `threadId` → Carina `session_id` for subsequent `command.exec`.\n */\n ensureSession(request: CarinaEnsureSessionRequest): Promise<string>;\n /** Peek cached Carina session id for a Sailor thread, if any. */\n resolveMappedSessionId(threadId: string): string | undefined;\n /**\n * Host HITL: map product approve/deny → Carina `governance.approval.resolve`.\n */\n resolveApproval(request: CarinaApprovalResolveRequest): Promise<unknown>;\n /** Lightweight connectivity probe (`gateway.hello`). */\n probe(): Promise<{ protocolVersion: number; ok: true }>;\n}\n\nfunction extractCarinaSessionId(session: unknown): string {\n if (!session || typeof session !== \"object\") {\n throw new CarinaProtocolError(\"Carina session.create returned non-object Session\");\n }\n const rec = session as Record<string, unknown>;\n for (const key of [\"id\", \"session_id\", \"sessionId\"] as const) {\n const v = rec[key];\n if (typeof v === \"string\" && v.length > 0) return v;\n }\n throw new CarinaProtocolError(\"Carina session.create Session missing id/session_id/sessionId\");\n}\n\n/**\n * Track-B adapter: Sailor control plane → **Carina** public RPC catalog (v0.8.x).\n *\n * Mapping (Sailor → Carina, never the reverse ownership):\n * - `threadId` → `command.exec` `session_id` (via ensureSession cache, else threadId)\n * - `command` → `argv` via `/bin/sh -c` (kernel still gates the joined command string)\n * - denied / requires_approval → {@link SandboxDelegationError} (fail closed)\n * - allowed + CommandResult → {@link SandboxExecResult}\n * - host lifecycle → `session.create` via {@link CarinaSandbox.ensureSession}\n *\n * Kernel protocol is maintained in `Nebutra/carina`. This file only maps.\n *\n * @see docs/architecture/2026-08-03-carina-track-b-upstream.md\n */\nexport function createCarinaSandbox(options: CarinaSandboxOptions): CarinaSandbox {\n const {\n baseUrl,\n socketPath,\n token,\n fetchImpl = fetch,\n rpcPath = \"\",\n minProtocolVersion = CARINA_MIN_PROTOCOL_VERSION,\n skipHello = false,\n executedOn = \"carina\",\n resolveSessionId,\n clientId = \"nebutra-sailor\",\n autoApproveOnRequire = false,\n } = options;\n\n const httpBase = baseUrl?.trim();\n const sock = socketPath?.trim();\n if (!httpBase && !sock) {\n throw new CarinaProtocolError(\n \"createCarinaSandbox requires baseUrl (HTTP) or socketPath (unix NDJSON)\",\n );\n }\n\n /** Sailor threadId → Carina session_id */\n const sessionByThread = new Map<string, string>();\n\n const endpoint = httpBase\n ? `${httpBase.replace(/\\/$/, \"\")}${\n !rpcPath ? \"\" : rpcPath.startsWith(\"/\") ? rpcPath : `/${rpcPath}`\n }`\n : \"\";\n\n let helloDone: Promise<void> | null = null;\n let rpcId = 0;\n let ndjson: NdjsonRpcClient | null = null;\n\n function getNdjson(): NdjsonRpcClient {\n if (!sock) {\n throw new CarinaProtocolError(\"socket transport requested without socketPath\");\n }\n if (!ndjson) ndjson = createCarinaNdjsonClient(sock);\n return ndjson;\n }\n\n async function rpcCallHttp<T>(method: string, params: Record<string, unknown>): Promise<T> {\n const id = ++rpcId;\n const headers = new Headers({ \"content-type\": \"application/json\" });\n if (token) {\n headers.set(\"authorization\", `Bearer ${token}`);\n }\n const response = await fetchImpl(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n jsonrpc: \"2.0\",\n id,\n method,\n params,\n }),\n });\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new SandboxDelegationError(\n `Carina transport error (${response.status}): ${detail}`,\n response.status,\n \"transport_error\",\n );\n }\n const body = (await response.json()) as JsonRpcResponse<T>;\n if (body.error) {\n throw new SandboxDelegationError(\n `Carina RPC ${method} failed (${body.error.code}): ${body.error.message}`,\n 502,\n String(body.error.code),\n );\n }\n if (!(\"result\" in body)) {\n throw new CarinaProtocolError(`Carina RPC ${method} returned no result`);\n }\n return body.result;\n }\n\n async function rpcCall<T>(method: string, params: Record<string, unknown>): Promise<T> {\n if (sock && !httpBase) {\n try {\n return await getNdjson().call<T>(method, params);\n } catch (err) {\n if (err instanceof CarinaNdjsonError) {\n throw new SandboxDelegationError(err.message, err.status, err.code);\n }\n throw err;\n }\n }\n return rpcCallHttp<T>(method, params);\n }\n\n async function ensureHello(): Promise<void> {\n if (skipHello) return;\n if (!helloDone) {\n helloDone = (async () => {\n const hello = await rpcCall<CarinaHello>(\"gateway.hello\", {\n protocol_version: minProtocolVersion,\n client_id: clientId,\n });\n const version = hello.protocol_version ?? 0;\n if (typeof version !== \"number\" || version < minProtocolVersion) {\n throw new CarinaProtocolError(\n `Carina protocol_version ${String(hello.protocol_version)} ` +\n `below required ${minProtocolVersion}`,\n );\n }\n })().catch((err) => {\n helloDone = null;\n throw err;\n });\n }\n await helloDone;\n }\n\n const defaultResolveSessionId = (r: SandboxExecRequest): string =>\n sessionByThread.get(r.threadId) ?? r.threadId;\n const mapSessionId = resolveSessionId ?? defaultResolveSessionId;\n\n return {\n resolveMappedSessionId(threadId: string): string | undefined {\n return sessionByThread.get(threadId);\n },\n\n async probe(): Promise<{ protocolVersion: number; ok: true }> {\n await ensureHello();\n // hello already validated version; re-call for an explicit probe result\n const hello = await rpcCall<CarinaHello>(\"gateway.hello\", {\n protocol_version: minProtocolVersion,\n client_id: clientId,\n });\n return { protocolVersion: hello.protocol_version ?? minProtocolVersion, ok: true };\n },\n\n async resolveApproval(request: CarinaApprovalResolveRequest): Promise<unknown> {\n const decisionId = request.decisionId.trim();\n if (!decisionId) {\n throw new CarinaProtocolError(\"resolveApproval requires decisionId\");\n }\n await ensureHello();\n const params: Record<string, unknown> = {\n decision_id: decisionId,\n approve: request.approve,\n };\n if (request.approver) params.approver = request.approver;\n if (request.scope) params.scope = request.scope;\n return rpcCall<unknown>(\"governance.approval.resolve\", params);\n },\n\n async ensureSession(request: CarinaEnsureSessionRequest): Promise<string> {\n const cached = sessionByThread.get(request.threadId);\n if (cached) return cached;\n\n const root = request.workspaceRoot.trim();\n if (!root) {\n throw new CarinaProtocolError(\"Carina session.create requires a non-empty workspaceRoot\");\n }\n\n await ensureHello();\n\n const params: Record<string, unknown> = { workspace_root: root };\n if (request.profile) params.profile = request.profile;\n if (request.approvalMode) params.approval_mode = request.approvalMode;\n // Audit hint only — not a privilege grant (cloud boundary).\n if (request.tenantId) params.tenant_id = request.tenantId;\n params.correlation_id = request.threadId;\n\n const session = await rpcCall<unknown>(\"session.create\", params);\n const sessionId = extractCarinaSessionId(session);\n sessionByThread.set(request.threadId, sessionId);\n return sessionId;\n },\n\n async exec(request: SandboxExecRequest): Promise<SandboxExecResult> {\n assertSafePosture(request.capabilityPolicy);\n\n await ensureHello();\n\n const sessionId = mapSessionId(request);\n const argv = [\"/bin/sh\", \"-c\", request.command];\n let wire = await rpcCall<CarinaExecWire>(\"command.exec\", {\n session_id: sessionId,\n argv,\n // Host correlation for audit — ignored if kernel drops unknown fields.\n correlation_id: `${request.tenantId}:${request.threadId}`,\n tenant_id: request.tenantId,\n });\n\n let decision = wire.decision?.decision ?? \"denied\";\n let decisionId = wire.decision?.decision_id;\n\n if (decision === \"denied\") {\n throw new SandboxDelegationError(\n `Carina denied command: ${wire.decision?.reason ?? \"denied\"}`,\n 403,\n \"denied\",\n );\n }\n if (decision === \"requires_approval\") {\n if (autoApproveOnRequire && decisionId) {\n await rpcCall<unknown>(\"governance.approval.resolve\", {\n decision_id: decisionId,\n approve: true,\n scope: \"once\",\n approver: clientId,\n });\n // Retry once after host auto-approve.\n wire = await rpcCall<CarinaExecWire>(\"command.exec\", {\n session_id: sessionId,\n argv,\n correlation_id: `${request.tenantId}:${request.threadId}`,\n tenant_id: request.tenantId,\n });\n const retryDecision = wire.decision?.decision ?? \"denied\";\n if (retryDecision === \"allowed\") {\n decision = \"allowed\";\n decisionId = wire.decision?.decision_id ?? decisionId;\n // fall through to result handling below\n } else if (retryDecision === \"denied\") {\n throw new SandboxDelegationError(\n `Carina denied command after auto-approve: ${wire.decision?.reason ?? \"denied\"}`,\n 403,\n \"denied\",\n );\n } else {\n throw new SandboxDelegationError(\n `Carina still requires approval after auto-approve` +\n (wire.decision?.decision_id ? ` (decision_id=${wire.decision.decision_id})` : \"\"),\n 409,\n \"requires_approval\",\n );\n }\n } else {\n throw new SandboxDelegationError(\n `Carina requires approval` + (decisionId ? ` (decision_id=${decisionId})` : \"\"),\n 409,\n \"requires_approval\",\n );\n }\n }\n\n const cr = wire.result;\n if (!cr) {\n throw new SandboxDelegationError(\n \"Carina allowed exec but returned no CommandResult\",\n 502,\n \"missing_result\",\n );\n }\n\n const stdout = (cr.stdout ?? []).join(\"\");\n const stderr = (cr.stderr ?? []).join(\"\");\n const aggregatedOutput =\n stderr.length > 0 ? `${stdout}${stdout ? \"\\n\" : \"\"}${stderr}` : stdout;\n\n return {\n exitCode: cr.exit_code ?? (cr.timed_out ? 124 : 0),\n aggregatedOutput,\n executedOn,\n // exactOptionalPropertyTypes: omit key when undefined\n ...(decisionId !== undefined ? { decisionId } : {}),\n correlationId: `${request.tenantId}:${request.threadId}`,\n };\n },\n };\n}\n\n/** Guard: reject the most dangerous posture unless explicitly opted in. */\nexport function assertSafePosture(policy: CapabilityPolicy, allowDanger = false): void {\n if (policy.kind === \"danger_full_access\" && !allowDanger) {\n throw new Error(\n \"danger_full_access capability policy refused: not permitted for \" +\n \"multi-tenant delegation without explicit opt-in.\",\n );\n }\n}\n\n// ── Env resolution + command-exec tool (Phase 2 host helpers) ────────────────\n\n/** Env bag for Carina host config. Compatible with `process.env`. */\nexport type CarinaEnv = Readonly<Record<string, string | undefined>>;\n\n/**\n * Resolve workspace path for a tenant/thread.\n * Order: CARINA_WORKSPACE_MAP[tenant] → TEMPLATE → CARINA_WORKSPACE_ROOT.\n */\nexport function resolveCarinaWorkspaceRoot(\n tenantId: string,\n env: CarinaEnv = process.env,\n threadId = \"\",\n): string | undefined {\n const mapRaw = env.CARINA_WORKSPACE_MAP?.trim();\n if (mapRaw) {\n try {\n const map = JSON.parse(mapRaw) as Record<string, unknown>;\n const hit = map[tenantId];\n if (typeof hit === \"string\" && hit.trim()) return hit.trim();\n } catch {\n // ignore bad JSON — fall through\n }\n }\n const template = env.CARINA_WORKSPACE_TEMPLATE?.trim();\n if (template) {\n return template.replaceAll(\"{tenantId}\", tenantId).replaceAll(\"{threadId}\", threadId).trim();\n }\n const root = env.CARINA_WORKSPACE_ROOT?.trim();\n return root || undefined;\n}\n\n/**\n * Co-deploy is on unless explicitly disabled (`CARINA_CODEPLOY=0|false`).\n * Same-host default: unix socket {@link CODEPLOY_CARINA_SOCKET_PATH}.\n */\nexport function isCarinaCodeployEnabled(env: CarinaEnv = process.env): boolean {\n const v = env.CARINA_CODEPLOY?.trim().toLowerCase();\n if (v === \"0\" || v === \"false\" || v === \"off\") return false;\n // Explicit enable, or any socket/url/path hint, or default co-deploy on Node hosts.\n if (v === \"1\" || v === \"true\" || v === \"on\") return true;\n if (env.CARINA_JSONRPC_URL?.trim()) return true;\n if (env.CARINA_DAEMON_SOCK?.trim()) return true;\n // Default: co-deploy mode for production Node gateway (opt-out with CARINA_CODEPLOY=0).\n return v === undefined || v === \"\";\n}\n\n/**\n * Resolve Track-B sandbox from environment.\n *\n * Priority:\n * 1. `CARINA_JSONRPC_URL` → HTTP JSON-RPC\n * 2. `CARINA_DAEMON_SOCK` → unix NDJSON\n * 3. co-deploy default → {@link CODEPLOY_CARINA_SOCKET_PATH}\n * 4. developer home socket → `~/.carina/daemon.sock` when co-deploy and no prod path intent\n * 5. otherwise {@link REFUSING_SANDBOX}\n */\nexport function resolveCarinaSandboxFromEnv(\n env: CarinaEnv = process.env,\n overrides: Partial<CarinaSandboxOptions> = {},\n): ExternalSandbox {\n const auto = env.CARINA_AUTO_APPROVE === \"1\" || env.CARINA_AUTO_APPROVE === \"true\";\n const common: Partial<CarinaSandboxOptions> = {\n ...(env.CARINA_CLIENT_ID ? { clientId: env.CARINA_CLIENT_ID } : {}),\n ...(auto ? { autoApproveOnRequire: true } : {}),\n ...overrides,\n };\n\n const baseUrl = env.CARINA_JSONRPC_URL?.trim();\n if (baseUrl) {\n return createCarinaSandbox({\n baseUrl,\n ...(env.CARINA_JSONRPC_TOKEN ? { token: env.CARINA_JSONRPC_TOKEN } : {}),\n ...(env.CARINA_JSONRPC_PATH ? { rpcPath: env.CARINA_JSONRPC_PATH } : {}),\n ...common,\n });\n }\n\n if (!isCarinaCodeployEnabled(env) && !env.CARINA_DAEMON_SOCK?.trim()) {\n return REFUSING_SANDBOX;\n }\n\n const sock =\n env.CARINA_DAEMON_SOCK?.trim() ||\n (env.CARINA_CODEPLOY_HOME === \"1\" || env.CARINA_CODEPLOY_HOME === \"true\"\n ? defaultCarinaSocketPath()\n : CODEPLOY_CARINA_SOCKET_PATH);\n\n return createCarinaSandbox({\n socketPath: sock,\n ...common,\n });\n}\n\n/** Default workspace when co-deploy is enabled and no override is set. */\nexport function resolveCarinaWorkspaceRootWithCodeploy(\n tenantId: string,\n env: CarinaEnv = process.env,\n threadId = \"\",\n): string | undefined {\n return (\n resolveCarinaWorkspaceRoot(tenantId, env, threadId) ??\n (isCarinaCodeployEnabled(env) ? CODEPLOY_CARINA_WORKSPACE_ROOT : undefined)\n );\n}\n\nexport function isCarinaSandbox(sandbox: ExternalSandbox): sandbox is CarinaSandbox {\n return (\n typeof (sandbox as CarinaSandbox).ensureSession === \"function\" &&\n typeof (sandbox as CarinaSandbox).resolveMappedSessionId === \"function\"\n );\n}\n\nexport {\n CODEPLOY_CARINA_SOCKET_PATH,\n CODEPLOY_CARINA_WORKSPACE_ROOT,\n defaultCarinaSocketPath,\n} from \"./carina-ndjson\";\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CapabilityPolicy } from './policy.js';
|
|
3
|
+
import { E as ExternalSandbox } from './sandbox-DfcRttXt.js';
|
|
4
|
+
import { RuntimeToolRegistry } from './tools.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Native command-exec tool that delegates to {@link ExternalSandbox}.
|
|
8
|
+
* Product Track B: pair with {@link createCarinaSandbox} + ensureSession.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
declare const COMMAND_EXEC_TOOL_NAME: "command_exec";
|
|
12
|
+
declare const commandExecInputSchema: z.ZodObject<{
|
|
13
|
+
command: z.ZodString;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
type CommandExecToolInput = z.infer<typeof commandExecInputSchema>;
|
|
16
|
+
type CommandExecToolOutput = {
|
|
17
|
+
readonly exitCode: number;
|
|
18
|
+
readonly aggregatedOutput: string;
|
|
19
|
+
readonly executedOn: string;
|
|
20
|
+
readonly decisionId?: string;
|
|
21
|
+
};
|
|
22
|
+
type RegisterCommandExecToolOptions = {
|
|
23
|
+
readonly sandbox: ExternalSandbox;
|
|
24
|
+
/**
|
|
25
|
+
* Absolute workspace on the Carina host. Required when sandbox is Carina
|
|
26
|
+
* (session.create). Gateway reads `CARINA_WORKSPACE_ROOT`.
|
|
27
|
+
*/
|
|
28
|
+
readonly workspaceRoot?: string;
|
|
29
|
+
readonly capabilityPolicy?: CapabilityPolicy;
|
|
30
|
+
readonly profile?: string;
|
|
31
|
+
readonly approvalMode?: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Register `command_exec` on a tool registry. For Carina sandboxes, ensures a
|
|
35
|
+
* session for the turn thread before `command.exec`.
|
|
36
|
+
*/
|
|
37
|
+
declare function registerCommandExecTool(registry: RuntimeToolRegistry, options: RegisterCommandExecToolOptions): void;
|
|
38
|
+
/**
|
|
39
|
+
* Build a registry that includes `command_exec` delegated to the given sandbox.
|
|
40
|
+
* Prefer this only when a real isolator is configured; with
|
|
41
|
+
* {@link REFUSING_SANDBOX} the tool will throw NoExecutorConfiguredError on use.
|
|
42
|
+
*/
|
|
43
|
+
declare function buildSandboxToolRegistry(sandbox: ExternalSandbox, options?: Omit<RegisterCommandExecToolOptions, "sandbox">): RuntimeToolRegistry;
|
|
44
|
+
|
|
45
|
+
export { COMMAND_EXEC_TOOL_NAME, type CommandExecToolInput, type CommandExecToolOutput, type RegisterCommandExecToolOptions, buildSandboxToolRegistry, registerCommandExecTool };
|