@corenel/harness 0.17.0 → 0.19.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/dist/core/pricing.d.ts +12 -0
- package/dist/core/pricing.d.ts.map +1 -1
- package/dist/core/pricing.js +8 -3
- package/dist/core/pricing.js.map +1 -1
- package/dist/prompts/library/defaults.generated.js +9 -9
- package/dist/prompts/library/defaults.generated.js.map +1 -1
- package/dist/sidecar/host.d.ts +41 -5
- package/dist/sidecar/host.d.ts.map +1 -1
- package/dist/sidecar/host.js +136 -3
- package/dist/sidecar/host.js.map +1 -1
- package/dist/sidecar/identity.d.ts +4 -0
- package/dist/sidecar/identity.d.ts.map +1 -1
- package/dist/sidecar/memberGate.d.ts.map +1 -1
- package/dist/sidecar/memberGate.js +5 -0
- package/dist/sidecar/memberGate.js.map +1 -1
- package/dist/sidecar/server.d.ts +42 -2
- package/dist/sidecar/server.d.ts.map +1 -1
- package/dist/sidecar/server.js +208 -5
- package/dist/sidecar/server.js.map +1 -1
- package/dist/tools/urlGuard.d.ts +11 -2
- package/dist/tools/urlGuard.d.ts.map +1 -1
- package/dist/tools/urlGuard.js +70 -15
- package/dist/tools/urlGuard.js.map +1 -1
- package/dist/workflows/results/resultFile.d.ts +23 -0
- package/dist/workflows/results/resultFile.d.ts.map +1 -0
- package/dist/workflows/results/resultFile.js +70 -0
- package/dist/workflows/results/resultFile.js.map +1 -0
- package/dist/workflows/results/resultStore.d.ts +37 -0
- package/dist/workflows/results/resultStore.d.ts.map +1 -0
- package/dist/workflows/results/resultStore.js +191 -0
- package/dist/workflows/results/resultStore.js.map +1 -0
- package/package.json +2 -2
package/dist/sidecar/host.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { asEcPublicJwk, jwkThumbprint, verifySignature, randomToken, serverProofPayload, clientProofPayload, parseEnrollmentCode, bytesToB64url, codeProof, codeHash, } from './deviceKeys.js';
|
|
2
2
|
import { capabilityMissingMessage } from './hostCapability.js';
|
|
3
|
-
import { SIDECAR_PROTOCOL, HOST_METHODS, errMessage, RUN_TASK_FEATURE, RUN_PLAN_FEATURE, CREW_DEPLOY_FEATURE, DEVICES_FEATURE, AGENT_ACCESS_FEATURE, AGENT_RUNS_FEATURE, PEER_LINKS_FEATURE, CHILD_EVENTS_FEATURE, PEER_DAEMON_FEATURE, TEAM_MEMBERS_FEATURE, SPEECH_FEATURE, encodePcm16, isClerkOrgId, } from '@corenel/protocol';
|
|
3
|
+
import { SIDECAR_PROTOCOL, HOST_METHODS, errMessage, RUN_TASK_FEATURE, RUN_PLAN_FEATURE, CREW_DEPLOY_FEATURE, DEVICES_FEATURE, AGENT_ACCESS_FEATURE, AGENT_RUNS_FEATURE, PEER_LINKS_FEATURE, CHILD_EVENTS_FEATURE, PEER_DAEMON_FEATURE, TEAM_MEMBERS_FEATURE, SPEECH_FEATURE, encodePcm16, isClerkOrgId, WORKFLOW_RUNNER_FEATURE, readWorkflowOkReply, readWorkflowResultsListReply, readWorkflowResultsTakeReply, } from '@corenel/protocol';
|
|
4
|
+
const NO_WORKFLOWS = 'this daemon does not run workflows';
|
|
5
|
+
const WRONG_REPLY = 'the daemon answered with the wrong reply';
|
|
6
|
+
/** A reply that failed its check (protocol workflowWire.ts readers). */
|
|
7
|
+
const MALFORMED_REPLY = 'the daemon sent a reply this app cannot read';
|
|
8
|
+
/** A workflow request the daemon has not answered by now never will be on
|
|
9
|
+
* this connection (a lost reply): it settles not ok, so the caller (the
|
|
10
|
+
* copy-home pass, say) is freed rather than held until the connection drops
|
|
11
|
+
* (M8). A package put carries up to 8 MiB, so it waits longer, sized for a
|
|
12
|
+
* slow link. */
|
|
13
|
+
export const WORKFLOW_REQUEST_TIMEOUT_MS = 60000;
|
|
14
|
+
export const WORKFLOW_PUT_TIMEOUT_MS = 120000;
|
|
15
|
+
const NO_ANSWER = 'the sidecar did not answer in time';
|
|
4
16
|
/** Decode a base64 chunk to bytes (atob is available in the browser and Node 18+). */
|
|
5
17
|
function b64ToBytes(b64) {
|
|
6
18
|
const bin = atob(b64);
|
|
@@ -236,6 +248,20 @@ export class SidecarHost {
|
|
|
236
248
|
writable: true,
|
|
237
249
|
value: new Map()
|
|
238
250
|
});
|
|
251
|
+
Object.defineProperty(this, "workflowsSeq", {
|
|
252
|
+
enumerable: true,
|
|
253
|
+
configurable: true,
|
|
254
|
+
writable: true,
|
|
255
|
+
value: 0
|
|
256
|
+
});
|
|
257
|
+
/** One pending map for the five workflow-* requests; each method checks it
|
|
258
|
+
* got its own reply type, then checks and rebuilds the reply itself. */
|
|
259
|
+
Object.defineProperty(this, "workflowsPending", {
|
|
260
|
+
enumerable: true,
|
|
261
|
+
configurable: true,
|
|
262
|
+
writable: true,
|
|
263
|
+
value: new Map()
|
|
264
|
+
});
|
|
239
265
|
Object.defineProperty(this, "accessSeq", {
|
|
240
266
|
enumerable: true,
|
|
241
267
|
configurable: true,
|
|
@@ -798,7 +824,9 @@ export class SidecarHost {
|
|
|
798
824
|
const proven = this.verifiedSidecarJkt !== null && this.provenSidecarId !== null && m.sidecarId === this.provenSidecarId;
|
|
799
825
|
const jkt = proven ? this.opts.deviceKey?.jkt : undefined;
|
|
800
826
|
const orgId = isClerkOrgId(m.orgId) ? m.orgId : undefined;
|
|
801
|
-
|
|
827
|
+
// The daemon's trusted issuer, for explaining a refusal only; bounded.
|
|
828
|
+
const issuer = typeof m.issuer === 'string' && m.issuer.length <= 512 ? m.issuer : undefined;
|
|
829
|
+
assertion = jkt ? (await this.opts.onIdentityChallenge?.({ nonce: m.nonce, sidecarId: m.sidecarId, jkt, ...(orgId ? { orgId } : {}), ...(issuer ? { issuer } : {}) })) ?? null : null;
|
|
802
830
|
}
|
|
803
831
|
catch {
|
|
804
832
|
assertion = null;
|
|
@@ -867,6 +895,9 @@ export class SidecarHost {
|
|
|
867
895
|
for (const p of this.crewDeployPending.values())
|
|
868
896
|
p.reject(new Error(reason));
|
|
869
897
|
this.crewDeployPending.clear();
|
|
898
|
+
for (const p of this.workflowsPending.values())
|
|
899
|
+
p.reject(new Error(reason));
|
|
900
|
+
this.workflowsPending.clear();
|
|
870
901
|
for (const p of this.accessPending.values())
|
|
871
902
|
p.reject(new Error(reason));
|
|
872
903
|
this.accessPending.clear();
|
|
@@ -1502,6 +1533,18 @@ export class SidecarHost {
|
|
|
1502
1533
|
p.resolve(m.reply);
|
|
1503
1534
|
return;
|
|
1504
1535
|
}
|
|
1536
|
+
case 'workflow-package-result':
|
|
1537
|
+
case 'workflow-results-list-result':
|
|
1538
|
+
case 'workflow-results-take-result':
|
|
1539
|
+
case 'workflow-results-ack-result':
|
|
1540
|
+
case 'workflow-stop-result': {
|
|
1541
|
+
const p = this.workflowsPending.get(m.id);
|
|
1542
|
+
if (!p)
|
|
1543
|
+
return;
|
|
1544
|
+
this.workflowsPending.delete(m.id);
|
|
1545
|
+
p.resolve(m);
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1505
1548
|
case 'member-result': {
|
|
1506
1549
|
const p = this.memberReplies.get(m.id);
|
|
1507
1550
|
if (!p)
|
|
@@ -1515,6 +1558,7 @@ export class SidecarHost {
|
|
|
1515
1558
|
...(m.adminLabel !== undefined ? { adminLabel: m.adminLabel } : {}),
|
|
1516
1559
|
...(m.adminId !== undefined ? { adminId: m.adminId } : {}),
|
|
1517
1560
|
...(m.admin !== undefined ? { admin: m.admin } : {}),
|
|
1561
|
+
...(m.noAdmin === true ? { noAdmin: true } : {}),
|
|
1518
1562
|
...(m.members ? { members: m.members } : {}),
|
|
1519
1563
|
}
|
|
1520
1564
|
: { ok: false, code: m.code, error: m.error });
|
|
@@ -2375,12 +2419,101 @@ export class SidecarHost {
|
|
|
2375
2419
|
}
|
|
2376
2420
|
/** The machine's deploy list, statuses and audit. */
|
|
2377
2421
|
crewDeployments() { return this.crewDeployRequest((id) => ({ t: 'crew-deployments', id })); }
|
|
2378
|
-
|
|
2422
|
+
/** Deploy an agent or a workflow. `workspaceId`: where a deployed
|
|
2423
|
+
* WORKFLOW's results go (the daemon refuses it on an agent). */
|
|
2424
|
+
crewDeploy(ref, reason, workspaceId) {
|
|
2425
|
+
return this.crewDeployRequest((id) => ({ t: 'crew-deploy', id, ref, ...(reason ? { reason } : {}), ...(workspaceId ? { workspaceId } : {}) }));
|
|
2426
|
+
}
|
|
2379
2427
|
crewUndeploy(ref, reason) { return this.crewDeployRequest((id) => ({ t: 'crew-undeploy', id, ref, ...(reason ? { reason } : {}) })); }
|
|
2380
2428
|
crewPause(ref, reason) { return this.crewDeployRequest((id) => ({ t: 'crew-pause', id, ref, reason })); }
|
|
2381
2429
|
crewResume(ref, reason) { return this.crewDeployRequest((id) => ({ t: 'crew-resume', id, ref, ...(reason ? { reason } : {}) })); }
|
|
2382
2430
|
/** Stop a run AND record who stopped it and why (the plain `cancelRun` records nothing). */
|
|
2383
2431
|
crewStop(ref, runId, reason) { return this.crewDeployRequest((id) => ({ t: 'crew-stop', id, ref, runId, reason })); }
|
|
2432
|
+
/** True when the connected daemon runs deployed workflows (package intake,
|
|
2433
|
+
* the results outbox, and `workflow-stop`). Feature-detected from the welcome. */
|
|
2434
|
+
supportsWorkflows() { return this.features.includes(WORKFLOW_RUNNER_FEATURE); }
|
|
2435
|
+
/** One workflow-* request. A refusal resolves; only a dropped connection
|
|
2436
|
+
* rejects. Callers check the feature first, so nothing is sent to a daemon
|
|
2437
|
+
* without it. */
|
|
2438
|
+
/** One workflow-* request: its reply, or null when the daemon did not
|
|
2439
|
+
* answer within `timeoutMs` (the request is then forgotten, so a late reply
|
|
2440
|
+
* is dropped). Only a dropped connection rejects. */
|
|
2441
|
+
workflowsRequest(build, timeoutMs = WORKFLOW_REQUEST_TIMEOUT_MS) {
|
|
2442
|
+
const id = ++this.workflowsSeq;
|
|
2443
|
+
return new Promise((resolve, reject) => {
|
|
2444
|
+
const timer = setTimeout(() => {
|
|
2445
|
+
if (!this.workflowsPending.delete(id))
|
|
2446
|
+
return;
|
|
2447
|
+
resolve(null);
|
|
2448
|
+
}, timeoutMs);
|
|
2449
|
+
this.workflowsPending.set(id, {
|
|
2450
|
+
resolve: (m) => { clearTimeout(timer); resolve(m); },
|
|
2451
|
+
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
2452
|
+
});
|
|
2453
|
+
this.ch.send(build(id));
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
/** Send one packaged workflow (`.pdpkg`, base64) to the daemon. */
|
|
2457
|
+
async workflowPackagePut(req) {
|
|
2458
|
+
if (!this.supportsWorkflows())
|
|
2459
|
+
return { ok: false, error: NO_WORKFLOWS };
|
|
2460
|
+
const m = await this.workflowsRequest((id) => ({
|
|
2461
|
+
t: 'workflow-package-put', id, packageId: req.packageId, workspaceId: req.workspaceId, sha256: req.sha256, pkg: req.pkg,
|
|
2462
|
+
}), WORKFLOW_PUT_TIMEOUT_MS);
|
|
2463
|
+
if (!m)
|
|
2464
|
+
return { ok: false, error: NO_ANSWER };
|
|
2465
|
+
if (m.t !== 'workflow-package-result')
|
|
2466
|
+
return { ok: false, error: WRONG_REPLY };
|
|
2467
|
+
return readWorkflowOkReply(m.reply) ?? { ok: false, error: MALFORMED_REPLY };
|
|
2468
|
+
}
|
|
2469
|
+
/** The results waiting for `workspaceId` in this connection's outbox. Every
|
|
2470
|
+
* item is checked to be one of that workspace's own results. */
|
|
2471
|
+
async workflowResultsList(workspaceId) {
|
|
2472
|
+
if (!this.supportsWorkflows())
|
|
2473
|
+
return { ok: false, error: NO_WORKFLOWS };
|
|
2474
|
+
const m = await this.workflowsRequest((id) => ({ t: 'workflow-results-list', id, workspaceId }));
|
|
2475
|
+
if (!m)
|
|
2476
|
+
return { ok: false, error: NO_ANSWER };
|
|
2477
|
+
if (m.t !== 'workflow-results-list-result')
|
|
2478
|
+
return { ok: false, error: WRONG_REPLY };
|
|
2479
|
+
return readWorkflowResultsListReply(m.reply, workspaceId) ?? { ok: false, error: MALFORMED_REPLY };
|
|
2480
|
+
}
|
|
2481
|
+
/** One waiting result's text. Nothing is deleted until `workflowResultsAck`. */
|
|
2482
|
+
async workflowResultsTake(path) {
|
|
2483
|
+
if (!this.supportsWorkflows())
|
|
2484
|
+
return { ok: false, error: NO_WORKFLOWS };
|
|
2485
|
+
const m = await this.workflowsRequest((id) => ({ t: 'workflow-results-take', id, path }));
|
|
2486
|
+
if (!m)
|
|
2487
|
+
return { ok: false, error: NO_ANSWER };
|
|
2488
|
+
if (m.t !== 'workflow-results-take-result')
|
|
2489
|
+
return { ok: false, error: WRONG_REPLY };
|
|
2490
|
+
return readWorkflowResultsTakeReply(m.reply) ?? { ok: false, error: MALFORMED_REPLY };
|
|
2491
|
+
}
|
|
2492
|
+
/** The result at `path` is safely home: the daemon deletes it. */
|
|
2493
|
+
async workflowResultsAck(path) {
|
|
2494
|
+
if (!this.supportsWorkflows())
|
|
2495
|
+
return { ok: false, error: NO_WORKFLOWS };
|
|
2496
|
+
const m = await this.workflowsRequest((id) => ({ t: 'workflow-results-ack', id, path }));
|
|
2497
|
+
if (!m)
|
|
2498
|
+
return { ok: false, error: NO_ANSWER };
|
|
2499
|
+
if (m.t !== 'workflow-results-ack-result')
|
|
2500
|
+
return { ok: false, error: WRONG_REPLY };
|
|
2501
|
+
return readWorkflowOkReply(m.reply) ?? { ok: false, error: MALFORMED_REPLY };
|
|
2502
|
+
}
|
|
2503
|
+
/** Stop a running deployed workflow: every live run of `ref`, or only
|
|
2504
|
+
* `runId`. The daemon records who stopped it and why. */
|
|
2505
|
+
async workflowStop(ref, reason, workspaceId, runId) {
|
|
2506
|
+
if (!this.supportsWorkflows())
|
|
2507
|
+
return { ok: false, error: NO_WORKFLOWS };
|
|
2508
|
+
const m = await this.workflowsRequest((id) => ({
|
|
2509
|
+
t: 'workflow-stop', id, ref, reason, ...(workspaceId ? { workspaceId } : {}), ...(runId ? { runId } : {}),
|
|
2510
|
+
}));
|
|
2511
|
+
if (!m)
|
|
2512
|
+
return { ok: false, error: NO_ANSWER };
|
|
2513
|
+
if (m.t !== 'workflow-stop-result')
|
|
2514
|
+
return { ok: false, error: WRONG_REPLY };
|
|
2515
|
+
return readWorkflowOkReply(m.reply) ?? { ok: false, error: MALFORMED_REPLY };
|
|
2516
|
+
}
|
|
2384
2517
|
/** True when the connected daemon keeps per-agent access (team mode, Part B). */
|
|
2385
2518
|
supportsAgentAccess() { return this.features.includes(AGENT_ACCESS_FEATURE); }
|
|
2386
2519
|
/** One access-* request. `ok: false` is the daemon's considered answer and
|