humanish 0.111.2 → 0.111.3
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/ARCHITECTURE.md +1 -1
- package/CHANGELOG.md +17 -0
- package/dist/cli/commands/study-route-computer-use.js +3 -8
- package/dist/cli/commands/study-route-preview.js +4 -6
- package/dist/cli/commands/study-route-shared-world.js +6 -7
- package/dist/evidence/redaction.d.ts +2 -2
- package/dist/evidence/redaction.js +3 -3
- package/dist/routes/computer-use/bundle-parts.d.ts +1 -32
- package/dist/routes/computer-use/bundle-parts.js +5 -92
- package/dist/routes/computer-use/e2b-desktop/prepare.js +11 -4
- package/dist/routes/computer-use/fanout-bundle.js +3 -2
- package/dist/routes/computer-use/live-phase.js +3 -7
- package/dist/routes/computer-use/participant-deps.d.ts +46 -0
- package/dist/routes/computer-use/participant-deps.js +48 -0
- package/dist/routes/computer-use/participant-facts.d.ts +5 -4
- package/dist/routes/computer-use/participant-facts.js +37 -2
- package/dist/routes/computer-use/participant-feedback.d.ts +33 -0
- package/dist/routes/computer-use/participant-feedback.js +92 -0
- package/dist/routes/computer-use/participant-model.d.ts +11 -0
- package/dist/routes/computer-use/participant-model.js +15 -0
- package/dist/routes/computer-use/preflight.js +33 -65
- package/dist/routes/computer-use/result.js +13 -52
- package/dist/routes/computer-use/route.js +6 -7
- package/dist/routes/computer-use/setup.d.ts +2 -6
- package/dist/routes/computer-use/setup.js +47 -93
- package/dist/routes/computer-use/single-bundle.js +3 -2
- package/dist/routes/computer-use/types.d.ts +2 -2
- package/dist/routes/preview.js +2 -8
- package/dist/routes/scripted/bundle.d.ts +2 -6
- package/dist/routes/scripted/result.d.ts +2 -1
- package/dist/routes/scripted/result.js +4 -4
- package/dist/routes/scripted/route.js +13 -38
- package/dist/routes/scripted/setup.d.ts +2 -1
- package/dist/routes/scripted/setup.js +21 -28
- package/dist/routes/scripted/subject.d.ts +3 -18
- package/dist/routes/scripted/subject.js +22 -78
- package/dist/routes/scripted/types.d.ts +2 -2
- package/dist/routes/shared-world/bundle.js +36 -1
- package/dist/routes/shared-world/comms.d.ts +2 -1
- package/dist/routes/shared-world/comms.js +3 -7
- package/dist/routes/shared-world/handoff.d.ts +2 -2
- package/dist/routes/shared-world/handoff.js +1 -1
- package/dist/routes/shared-world/participant-specs.d.ts +4 -5
- package/dist/routes/shared-world/participant-specs.js +19 -25
- package/dist/routes/shared-world/plan.js +11 -7
- package/dist/routes/shared-world/provisioned.d.ts +2 -1
- package/dist/routes/shared-world/provisioned.js +37 -80
- package/dist/routes/shared-world/result.js +17 -35
- package/dist/routes/shared-world/route.js +33 -36
- package/dist/routes/shared-world/setup.d.ts +2 -3
- package/dist/routes/shared-world/setup.js +11 -29
- package/dist/routes/shared-world/types.d.ts +6 -15
- package/dist/routes/terminal/dry-run.js +14 -24
- package/dist/routes/terminal/live-finish.d.ts +0 -1
- package/dist/routes/terminal/live-finish.js +2 -2
- package/dist/routes/terminal/route.js +9 -15
- package/dist/routes/terminal/session.d.ts +2 -3
- package/dist/routes/terminal/session.js +28 -40
- package/dist/routes/terminal/trace.d.ts +1 -1
- package/dist/routes/terminal/types.d.ts +2 -2
- package/dist/run/run.d.ts +55 -1
- package/dist/run/run.js +38 -2
- package/dist/run/secrets.d.ts +22 -0
- package/dist/run/secrets.js +33 -0
- package/dist/run/study-result.d.ts +34 -0
- package/dist/run/study-result.js +16 -0
- package/dist/study/requirements.d.ts +60 -5
- package/dist/study/requirements.js +88 -2
- package/dist/study/run-study-events.d.ts +4 -3
- package/dist/study/run-study-events.js +9 -14
- package/dist/study/run-study-options.d.ts +4 -1
- package/dist/study/run-study-options.js +22 -32
- package/dist/substrates/e2b/subject-sandbox.d.ts +58 -0
- package/dist/substrates/e2b/subject-sandbox.js +94 -0
- package/docs/contracts/core.md +5 -4
- package/docs/contracts/feedback.md +12 -5
- package/package.json +1 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// The terminal route's dry run: a contract-only bundle with the persona and prompt digest bound,
|
|
2
2
|
// published through the run scope with no sandbox, key or spend. session.ts holds the live path.
|
|
3
|
-
import { digestText, redactText
|
|
3
|
+
import { digestText, redactText } from "../../evidence/redaction.js";
|
|
4
4
|
import { participantAssignment } from "../../study/participant-assignment.js";
|
|
5
|
-
import {
|
|
5
|
+
import {} from "../../run/bundle.js";
|
|
6
6
|
import { judgeExecution, judgeTerminal, OUTCOME_POLICIES, resultOk } from "../../run/judge.js";
|
|
7
7
|
import { validatePreparedRunArtifactPaths } from "../../run/paths.js";
|
|
8
|
+
import { RunSecrets } from "../../run/secrets.js";
|
|
8
9
|
import { buildTerminalProductBundle, renderTerminalReviewMarkdown } from "./bundle.js";
|
|
9
10
|
import { resolveTerminalPersona, terminalPersonaRef } from "./persona.js";
|
|
10
11
|
import { declaredRuntimeProvenance } from "./runtime.js";
|
|
11
|
-
import { defaultMission
|
|
12
|
+
import { defaultMission } from "./session.js";
|
|
12
13
|
import {} from "./types.js";
|
|
13
14
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
14
15
|
/**
|
|
@@ -18,32 +19,22 @@ import { studyResultIdentity } from "../../run/study-result.js";
|
|
|
18
19
|
export async function runDryTerminalStudy(args) {
|
|
19
20
|
const { plan, input, cwd, warnings, failed, scope } = args;
|
|
20
21
|
const { product } = plan;
|
|
21
|
-
const { evidenceMission, physicalCwd, persona } = await prepareDryPersona({
|
|
22
|
+
const { evidenceMission, physicalCwd, persona, secrets } = await prepareDryPersona({
|
|
22
23
|
plan,
|
|
23
24
|
cwd,
|
|
24
25
|
env: input.env ?? process.env,
|
|
25
26
|
warnings,
|
|
26
27
|
});
|
|
27
|
-
const started = await scope.startRun({
|
|
28
|
+
const started = await scope.startRun(plan, input, {
|
|
28
29
|
cwd: physicalCwd,
|
|
29
|
-
|
|
30
|
-
mintRunId: makeTerminalRunId,
|
|
31
|
-
mode: "dry-run",
|
|
32
|
-
study: plan.study,
|
|
33
|
-
warnings: plan.warnings,
|
|
30
|
+
prefix: "terminal",
|
|
34
31
|
renderReview: renderTerminalReviewMarkdown,
|
|
35
|
-
|
|
32
|
+
secrets,
|
|
36
33
|
});
|
|
37
34
|
if (!started.ok)
|
|
38
35
|
return failed(started.code, started.message);
|
|
39
36
|
const { run } = started;
|
|
40
|
-
const { runId, createdAt } = run;
|
|
41
|
-
const source = await buildRunSource({
|
|
42
|
-
capturedAt: createdAt,
|
|
43
|
-
cwd: physicalCwd,
|
|
44
|
-
humanishSource: "present",
|
|
45
|
-
packageName: "humanish",
|
|
46
|
-
});
|
|
37
|
+
const { runId, createdAt, source } = run;
|
|
47
38
|
const policies = plan.residual.policies;
|
|
48
39
|
const judgment = judgeTerminal({ dryRun: true, participant: undefined });
|
|
49
40
|
const bundle = buildTerminalProductBundle({
|
|
@@ -107,10 +98,9 @@ async function prepareDryPersona(args) {
|
|
|
107
98
|
const { plan, cwd, env, warnings } = args;
|
|
108
99
|
const { product } = plan;
|
|
109
100
|
const mission = plan.mission ?? defaultMission(product.name);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const evidenceMission = participantAssignment({ mission }, scrubLiterals(knownSecretValues)).mission;
|
|
101
|
+
// A dry run checks no key, so it scrubs every key the live run could read.
|
|
102
|
+
const secrets = new RunSecrets([env.CODEX_API_KEY, env.OPENAI_API_KEY, env.E2B_API_KEY].map((value) => value?.trim() ?? ""));
|
|
103
|
+
const evidenceMission = participantAssignment({ mission }, secrets.scrub).mission;
|
|
114
104
|
const terminalPersona = await resolveTerminalPersona({ plan, cwd, warnings });
|
|
115
105
|
// The composed prompt = mission + persona + public-surface manifest. Only the author mission
|
|
116
106
|
// goes plaintext into evidence (it is public-safe committed study text); the full composed prompt
|
|
@@ -121,9 +111,9 @@ async function prepareDryPersona(args) {
|
|
|
121
111
|
productName: product.name,
|
|
122
112
|
publicSurfaces: product.publicSurfaces,
|
|
123
113
|
});
|
|
124
|
-
const persona = terminalPersonaRef(terminalPersona, digestText(composedPrompt),
|
|
114
|
+
const persona = terminalPersonaRef(terminalPersona, digestText(composedPrompt), secrets.scrub);
|
|
125
115
|
const { physicalCwd } = terminalPersona;
|
|
126
|
-
return { evidenceMission, physicalCwd, persona };
|
|
116
|
+
return { evidenceMission, physicalCwd, persona, secrets };
|
|
127
117
|
}
|
|
128
118
|
/** The declared runtime provenance as a dry-run event; nothing is observed without a sandbox. */
|
|
129
119
|
function runtimeDeclaredEvent(runtime, createdAt) {
|
|
@@ -13,7 +13,6 @@ export interface LiveFinishInputs {
|
|
|
13
13
|
cwd: string;
|
|
14
14
|
sanitize: (text: string) => string;
|
|
15
15
|
nowIso: () => string;
|
|
16
|
-
knownSecretValues: string[];
|
|
17
16
|
runtimeEnv: LiveSandboxInputs["runtimeEnv"];
|
|
18
17
|
runtime: LiveSandboxInputs["runtime"];
|
|
19
18
|
persona: ActorPersonaRef;
|
|
@@ -14,7 +14,7 @@ import { terminalExecutionFailures, terminalStudyResult, terminalParticipantFact
|
|
|
14
14
|
import { parseTerminalTokenUsage } from "./token-usage.js";
|
|
15
15
|
import { buildTerminalActorTrace, scrubSplitKnownValues, tailOf } from "./trace.js";
|
|
16
16
|
function buildLiveTrace(inputs) {
|
|
17
|
-
const { persona, sanitize, nowIso, runtimeEnv, runtime
|
|
17
|
+
const { persona, sanitize, nowIso, runtimeEnv, runtime } = inputs;
|
|
18
18
|
const { product } = inputs.plan;
|
|
19
19
|
const { session } = inputs;
|
|
20
20
|
const { createdAt } = inputs.run;
|
|
@@ -22,7 +22,7 @@ function buildLiveTrace(inputs) {
|
|
|
22
22
|
// Prefix reconciliation may cut through a known key. Scrub literal values across the retained
|
|
23
23
|
// chunks before any transcript/trace/event artifact is persisted. Check both each stream and
|
|
24
24
|
// the combined event order that the transcript uses; either view can assemble a split value.
|
|
25
|
-
scrubSplitKnownValues(terminalEvents,
|
|
25
|
+
scrubSplitKnownValues(terminalEvents, inputs.run.secrets.values(), discardedPrefixes);
|
|
26
26
|
// Build the actor trace first (the cost ledger reads its tokenUsage). A session past the cap
|
|
27
27
|
// ends its transcript with what was not stored, and its trace says the same.
|
|
28
28
|
const cut = inputs.recorder.transcriptCut();
|
|
@@ -53,21 +53,20 @@ import { checkLiveTerminalMachine, runLiveTerminalSession } from "./session.js";
|
|
|
53
53
|
import { terminalRouteScorer } from "../../study/adapter-scorer-loader.js";
|
|
54
54
|
import { withLateScorer } from "../../study/route-inputs.js";
|
|
55
55
|
import {} from "./types.js";
|
|
56
|
-
import {
|
|
56
|
+
import { refusedResult } from "../../run/study-result.js";
|
|
57
57
|
/** A refused terminal study's result: the route's envelope, and the analysis record a refusal gets. */
|
|
58
58
|
export function terminalStudyRefusal(options, refusal) {
|
|
59
59
|
const { config, dryRun } = options;
|
|
60
|
-
const refused = {
|
|
61
|
-
|
|
62
|
-
ok: false,
|
|
60
|
+
const refused = refusedResult("terminal", {
|
|
61
|
+
studyId: config.id,
|
|
63
62
|
cwd: path.resolve(options.cwd),
|
|
63
|
+
error: { code: refusal.code, message: refusal.message },
|
|
64
|
+
}, {
|
|
64
65
|
actor: refusal.actor ?? config.actor?.type ?? "",
|
|
65
66
|
product: config.subject.product?.name ?? "",
|
|
66
67
|
dryRun,
|
|
67
68
|
runId: options.runId ?? "not-created",
|
|
68
|
-
|
|
69
|
-
error: { code: refusal.code, message: refusal.message },
|
|
70
|
-
};
|
|
69
|
+
});
|
|
71
70
|
return completeRefusalAnalysis(refused, config, options);
|
|
72
71
|
}
|
|
73
72
|
/**
|
|
@@ -86,11 +85,11 @@ export function admitTerminalPlan(plan, input) {
|
|
|
86
85
|
});
|
|
87
86
|
}
|
|
88
87
|
/** A live plan's local checks, made outside any run scope. */
|
|
89
|
-
function admitTerminalRun(plan, input) {
|
|
88
|
+
async function admitTerminalRun(plan, input) {
|
|
90
89
|
const warnings = [];
|
|
91
90
|
if (plan.dryRun)
|
|
92
91
|
return { ok: true, admitted: { plan, warnings } };
|
|
93
|
-
const checked = checkLiveTerminalMachine(plan, input, warnings);
|
|
92
|
+
const checked = await checkLiveTerminalMachine(plan, input, warnings);
|
|
94
93
|
if (checked.ok)
|
|
95
94
|
return { ok: true, admitted: { plan, warnings, runtimeEnv: checked.runtimeEnv } };
|
|
96
95
|
return {
|
|
@@ -101,16 +100,11 @@ function admitTerminalRun(plan, input) {
|
|
|
101
100
|
/** The route's envelope for a run that stops before its bundle. */
|
|
102
101
|
function terminalFailure(plan, input, warnings) {
|
|
103
102
|
const cwd = path.resolve(input.cwd);
|
|
104
|
-
return (code, message) => ({
|
|
105
|
-
...studyResultIdentity("terminal", plan.studyId),
|
|
106
|
-
ok: false,
|
|
107
|
-
cwd,
|
|
103
|
+
return (code, message) => refusedResult("terminal", { studyId: plan.studyId, cwd, warnings, error: { code, message } }, {
|
|
108
104
|
actor: plan.actor,
|
|
109
105
|
product: plan.product.name,
|
|
110
106
|
dryRun: plan.dryRun,
|
|
111
107
|
runId: input.runId ?? "not-created",
|
|
112
|
-
warnings,
|
|
113
|
-
error: { code, message },
|
|
114
108
|
});
|
|
115
109
|
}
|
|
116
110
|
async function runTerminalPlanInScope(admitted, input, scope) {
|
|
@@ -4,14 +4,14 @@ import type { LiveTerminalAuth, LiveTerminalPlan, RunLiveTerminalSessionArgs, Te
|
|
|
4
4
|
* E2B_API_KEY. They read only the plan and the environment, so the CLI makes them before it loads
|
|
5
5
|
* a declared scorer. Returns the runtime key's command-scoped placement, or the refusal.
|
|
6
6
|
*/
|
|
7
|
-
export declare function checkLiveTerminalMachine(plan: LiveTerminalPlan, input: TerminalRunInput, warnings: string[]): {
|
|
7
|
+
export declare function checkLiveTerminalMachine(plan: LiveTerminalPlan, input: TerminalRunInput, warnings: string[]): Promise<{
|
|
8
8
|
readonly ok: true;
|
|
9
9
|
readonly runtimeEnv: LiveTerminalAuth;
|
|
10
10
|
} | {
|
|
11
11
|
readonly ok: false;
|
|
12
12
|
readonly code: TerminalProductStudyErrorCode;
|
|
13
13
|
readonly message: string;
|
|
14
|
-
}
|
|
14
|
+
}>;
|
|
15
15
|
/**
|
|
16
16
|
* The live in-sandbox agent session orchestrator. Enforces the 8-point safety contract by
|
|
17
17
|
* construction; fails closed before any sandbox/key/spend on any precondition miss. Persists the substrate-lifecycle/command-log/interventions/cleanup ledgers,
|
|
@@ -21,4 +21,3 @@ export declare function checkLiveTerminalMachine(plan: LiveTerminalPlan, input:
|
|
|
21
21
|
export declare function runLiveTerminalSession(args: RunLiveTerminalSessionArgs): Promise<TerminalProductStudyResult>;
|
|
22
22
|
/** The default mission when the study omits one. Public-safe, product-neutral author text. */
|
|
23
23
|
export declare function defaultMission(productName: string): string;
|
|
24
|
-
export declare function makeTerminalRunId(): string;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { keysCheck } from "../../study/requirements.js";
|
|
3
3
|
import { declaredRuntimeProvenance } from "./runtime.js";
|
|
4
|
-
import { digestText, redactText
|
|
5
|
-
import {
|
|
6
|
-
import { buildRunSource } from "../../run/bundle.js";
|
|
4
|
+
import { digestText, redactText } from "../../evidence/redaction.js";
|
|
5
|
+
import { RunSecrets } from "../../run/secrets.js";
|
|
7
6
|
import { renderTerminalReviewMarkdown } from "./bundle.js";
|
|
8
7
|
import { buildRuntimeAuth, buildSandboxMetadata } from "./credentials.js";
|
|
9
8
|
import { resolveTerminalPersona, terminalPersonaRef } from "./persona.js";
|
|
@@ -15,7 +14,7 @@ import { finishLiveTerminalSession } from "./live-finish.js";
|
|
|
15
14
|
* E2B_API_KEY. They read only the plan and the environment, so the CLI makes them before it loads
|
|
16
15
|
* a declared scorer. Returns the runtime key's command-scoped placement, or the refusal.
|
|
17
16
|
*/
|
|
18
|
-
export function checkLiveTerminalMachine(plan, input, warnings) {
|
|
17
|
+
export async function checkLiveTerminalMachine(plan, input, warnings) {
|
|
19
18
|
const env = input.env ?? process.env;
|
|
20
19
|
const { maxUsd } = plan.caps;
|
|
21
20
|
// planTerminalStudy refuses a positive maxUsd without a costProbe, so one is present here.
|
|
@@ -28,13 +27,14 @@ export function checkLiveTerminalMachine(plan, input, warnings) {
|
|
|
28
27
|
return runtimeEnv;
|
|
29
28
|
// The sandbox is created with E2B_API_KEY, so a missing key is refused before the run starts.
|
|
30
29
|
// The runtime key above is the plan's `key-one-of`; E2B_API_KEY is its only `key`.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
const keys = await keysCheck({
|
|
31
|
+
requirements: plan.requirements,
|
|
32
|
+
env,
|
|
33
|
+
code: "HUMANISH_TERMINAL_KEYS_MISSING",
|
|
34
|
+
need: (names) => `Live terminal-product studies need ${names} in the environment (values are never persisted).`,
|
|
35
|
+
});
|
|
36
|
+
if (keys)
|
|
37
|
+
return { ok: false, ...keys };
|
|
38
38
|
return { ok: true, runtimeEnv };
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
@@ -53,32 +53,26 @@ export async function runLiveTerminalSession(args) {
|
|
|
53
53
|
const nowIso = () => new Date(now()).toISOString();
|
|
54
54
|
const prepared = await prepareLivePrompt({ plan, cwd, runtimeEnv, env, warnings });
|
|
55
55
|
const { mission, physicalCwd, persona, composedPrompt, verdictNonce } = prepared;
|
|
56
|
-
const {
|
|
57
|
-
const started = await scope.startRun({
|
|
56
|
+
const { secrets, sanitize } = prepared;
|
|
57
|
+
const started = await scope.startRun(plan, input, {
|
|
58
58
|
cwd: physicalCwd,
|
|
59
|
-
|
|
60
|
-
mintRunId: makeTerminalRunId,
|
|
61
|
-
// This entry point is the live terminal route; its dry-run sibling is a separate function.
|
|
62
|
-
mode: "live",
|
|
63
|
-
study: plan.study,
|
|
64
|
-
warnings: plan.warnings,
|
|
59
|
+
prefix: "terminal",
|
|
65
60
|
renderReview: renderTerminalReviewMarkdown,
|
|
66
|
-
observer: { open: input.open === true, render: deps.renderObserver },
|
|
67
61
|
now,
|
|
62
|
+
secrets,
|
|
68
63
|
});
|
|
69
64
|
if (!started.ok)
|
|
70
65
|
return failed(started.code, started.message);
|
|
71
66
|
const { run } = started;
|
|
72
|
-
const { runId,
|
|
73
|
-
const source = await buildRunSource({
|
|
74
|
-
capturedAt: createdAt,
|
|
75
|
-
cwd: physicalCwd,
|
|
76
|
-
humanishSource: "present",
|
|
77
|
-
packageName: "humanish",
|
|
78
|
-
});
|
|
67
|
+
const { runId, paths: runPaths, source } = run;
|
|
79
68
|
const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
|
|
80
69
|
// The ledgers + capture buffers, mutated through the live lifecycle.
|
|
81
|
-
const recorder = createTerminalRecorder({
|
|
70
|
+
const recorder = createTerminalRecorder({
|
|
71
|
+
nowIso,
|
|
72
|
+
sanitize,
|
|
73
|
+
knownSecretValues: run.secrets.values(),
|
|
74
|
+
verdictNonce,
|
|
75
|
+
});
|
|
82
76
|
const { version, model, modelSource, reasoningEffort } = plan.runtime;
|
|
83
77
|
const runtime = declaredRuntimeProvenance({
|
|
84
78
|
...(version === undefined ? {} : { version }),
|
|
@@ -128,7 +122,6 @@ export async function runLiveTerminalSession(args) {
|
|
|
128
122
|
cwd,
|
|
129
123
|
sanitize,
|
|
130
124
|
nowIso,
|
|
131
|
-
knownSecretValues,
|
|
132
125
|
runtimeEnv,
|
|
133
126
|
runtime,
|
|
134
127
|
persona,
|
|
@@ -166,17 +159,16 @@ async function prepareLivePrompt(args) {
|
|
|
166
159
|
// anything persists (a key has no detectable "shape" if it is an arbitrary token); redactText is
|
|
167
160
|
// the second pass for secret-shaped content. Applied pre-truncation so a cut can never split a
|
|
168
161
|
// value past the scrubber.
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
const persona = terminalPersonaRef(terminalPersona, promptDigest, scrubKnownValues);
|
|
162
|
+
const secrets = new RunSecrets([runtimeEnv.keyValue, env.E2B_API_KEY?.trim() ?? ""]);
|
|
163
|
+
const sanitize = (text) => redactText(secrets.scrub(text));
|
|
164
|
+
const persona = terminalPersonaRef(terminalPersona, promptDigest, secrets.scrub);
|
|
173
165
|
return {
|
|
174
166
|
mission,
|
|
175
167
|
physicalCwd: terminalPersona.physicalCwd,
|
|
176
168
|
persona,
|
|
177
169
|
composedPrompt,
|
|
178
170
|
verdictNonce,
|
|
179
|
-
|
|
171
|
+
secrets,
|
|
180
172
|
sanitize,
|
|
181
173
|
};
|
|
182
174
|
}
|
|
@@ -197,7 +189,3 @@ function composeLivePrompt(args) {
|
|
|
197
189
|
export function defaultMission(productName) {
|
|
198
190
|
return `You are an autonomous agent. Discover ${productName} from its public surfaces and determine whether it can help with a durable real task. Stay within the declared no-spend caps. Leave feedback if the workflow is confusing.`;
|
|
199
191
|
}
|
|
200
|
-
export function makeTerminalRunId() {
|
|
201
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
202
|
-
return `terminal-${stamp}-${randomBytes(4).toString("hex")}`;
|
|
203
|
-
}
|
|
@@ -7,7 +7,7 @@ import { type CommandLogRecord, type TerminalEventRecord } from "./types.js";
|
|
|
7
7
|
* known values before persistence without collapsing events or changing stdout/stderr ordering.
|
|
8
8
|
* Work backwards through matches so edits to later text leave earlier offsets valid.
|
|
9
9
|
*/
|
|
10
|
-
export declare function scrubSplitKnownValues(events: TerminalEventRecord[], knownValues: string[], discardedPrefixes: Record<"stdout" | "stderr" | "combined", string>): void;
|
|
10
|
+
export declare function scrubSplitKnownValues(events: TerminalEventRecord[], knownValues: readonly string[], discardedPrefixes: Record<"stdout" | "stderr" | "combined", string>): void;
|
|
11
11
|
/** Redacted, ellipsis-prefixed tail of a captured stream/log for a message field. */
|
|
12
12
|
export declare function tailOf(text: string): string;
|
|
13
13
|
/**
|
|
@@ -8,7 +8,7 @@ import type { TerminalPlan } from "../../study/plan-types.js";
|
|
|
8
8
|
import type { StudyDeps } from "../../study/study-deps.js";
|
|
9
9
|
import type { ObserverResult } from "../../observer/render.js";
|
|
10
10
|
import { type RunAdapterScore, type RunBundle, type RunFeedbackCandidate, type RunScorerProvenance } from "../../run/bundle.js";
|
|
11
|
-
import { type StudyResultIdentity } from "../../run/study-result.js";
|
|
11
|
+
import { type StudyResultErrorCode, type StudyResultIdentity } from "../../run/study-result.js";
|
|
12
12
|
/** Provider-neutral metadata constant: the route's non-secret tag (mirrors CUA_ACTOR_STUDY_PROVIDER_METADATA). */
|
|
13
13
|
export declare const TERMINAL_PRODUCT_STUDY_PROVIDER_METADATA: {
|
|
14
14
|
readonly mode: "terminal-product-lab";
|
|
@@ -172,7 +172,7 @@ export interface TerminalProductStudyResult extends AutomaticAnalysisResult, Stu
|
|
|
172
172
|
observer?: ObserverResult;
|
|
173
173
|
warnings: string[];
|
|
174
174
|
error?: {
|
|
175
|
-
code:
|
|
175
|
+
code: StudyResultErrorCode | "HUMANISH_TERMINAL_FAILED" | "HUMANISH_TERMINAL_ACTOR_UNSUPPORTED" | "HUMANISH_TERMINAL_SUBJECT_INVALID" | "HUMANISH_TERMINAL_KEYPLACEMENT_INVALID" | "HUMANISH_TERMINAL_RUNTIME_AUTH_MISSING" | "HUMANISH_TERMINAL_KEYS_MISSING" | "HUMANISH_TERMINAL_CAPS_MISSING" | "HUMANISH_TERMINAL_CAPS_INVALID" | "HUMANISH_TERMINAL_UNPRICED_CAP" | "HUMANISH_TERMINAL_CAPS_EXCEEDED" | "HUMANISH_TERMINAL_CREDENTIAL_DENIED" | "HUMANISH_TERMINAL_CLEANUP_UNPROVEN";
|
|
176
176
|
message: string;
|
|
177
177
|
};
|
|
178
178
|
}
|
package/dist/run/run.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { renderObserver, type ObserverResult } from "../observer/render.js";
|
|
|
2
2
|
import { type RunBundle } from "./bundle.js";
|
|
3
3
|
import { type ExecutionFailure, type ExecutionOutcome, type OutcomePolicy } from "./judge.js";
|
|
4
4
|
import { type PreparedRunArtifactPaths, type RunIdInUse } from "./paths.js";
|
|
5
|
+
import { RunSecrets } from "./secrets.js";
|
|
5
6
|
import type { RunStudyProvenance } from "./study-provenance.js";
|
|
6
7
|
interface StartRunOptions {
|
|
7
8
|
/** Project directory, in the form the route resolved it. */
|
|
@@ -24,6 +25,41 @@ interface StartRunOptions {
|
|
|
24
25
|
now?: (() => number) | undefined;
|
|
25
26
|
/** Warnings about the study's own fields. Every bundle write records each as a warn event. */
|
|
26
27
|
warnings?: readonly string[] | undefined;
|
|
28
|
+
/** The route's known values, created before the run so a refusal before it is scrubbed too. */
|
|
29
|
+
secrets?: RunSecrets | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** The prefix of the run ids each recorded route mints. Preview mints its own `dryrun-` ids. */
|
|
32
|
+
type RunIdPrefix = "cua" | "concurrent-shared-world" | "scripted" | "terminal";
|
|
33
|
+
/** What a recorded route's plan holds for its run's start. */
|
|
34
|
+
interface StartRunPlan {
|
|
35
|
+
readonly dryRun: boolean;
|
|
36
|
+
readonly study?: RunStudyProvenance | undefined;
|
|
37
|
+
readonly warnings?: readonly string[] | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** What a recorded route's input holds for its run's start. */
|
|
40
|
+
interface StartRunInput {
|
|
41
|
+
/** Caller-supplied id (`--run-id`, library `runId`); one is minted from the prefix when absent. */
|
|
42
|
+
readonly runId?: string | undefined;
|
|
43
|
+
/** Open the Observer page once it renders. */
|
|
44
|
+
readonly open?: boolean | undefined;
|
|
45
|
+
/** `renderObserver` is the seam FinishedRun.renderObserver calls. */
|
|
46
|
+
readonly deps?: {
|
|
47
|
+
readonly renderObserver?: typeof renderObserver | undefined;
|
|
48
|
+
} | undefined;
|
|
49
|
+
}
|
|
50
|
+
/** What a recorded route passes to startRun besides its plan and input. */
|
|
51
|
+
interface RouteRunOptions {
|
|
52
|
+
/** Project directory, in the form the route resolved it. */
|
|
53
|
+
cwd: string;
|
|
54
|
+
prefix: RunIdPrefix;
|
|
55
|
+
/** review.md for the published bundle. */
|
|
56
|
+
renderReview: (bundle: RunBundle) => string;
|
|
57
|
+
/** `none` when the route creates no sandbox for this run; status.json records it for reclaim. */
|
|
58
|
+
sandboxes?: "none" | undefined;
|
|
59
|
+
/** Clock for `createdAt` and the latest pointer. The minted id reads the wall clock. */
|
|
60
|
+
now?: (() => number) | undefined;
|
|
61
|
+
/** The route's known values, created before the run so a refusal before it is scrubbed too. */
|
|
62
|
+
secrets?: RunSecrets | undefined;
|
|
27
63
|
}
|
|
28
64
|
/** How a run ended as an execution, as run.json records it: the result's ok and execution outcome. */
|
|
29
65
|
interface RecordedOutcome {
|
|
@@ -46,6 +82,8 @@ interface Run {
|
|
|
46
82
|
readonly study?: RunStudyProvenance;
|
|
47
83
|
/** Routes write their evidence files through these and hand them to participants. */
|
|
48
84
|
readonly paths: PreparedRunArtifactPaths;
|
|
85
|
+
/** The literal values the route scrubs from this run's evidence; a value found later is added here. */
|
|
86
|
+
readonly secrets: RunSecrets;
|
|
49
87
|
/**
|
|
50
88
|
* Publish an in-progress bundle: run.json, review.json, review.md, events.ndjson and
|
|
51
89
|
* observer/observer-data.json, plus the latest pointer until one pointer write has succeeded,
|
|
@@ -74,11 +112,27 @@ interface Run {
|
|
|
74
112
|
*/
|
|
75
113
|
finish(bundle: RunBundle, outcome: FinishOutcome): Promise<FinishedRun>;
|
|
76
114
|
}
|
|
115
|
+
/** A recorded route's run, with the source its bundle records. */
|
|
116
|
+
interface RecordedRun extends Run {
|
|
117
|
+
/** The package and the project's git state, captured once the run exists, at its createdAt. */
|
|
118
|
+
readonly source: RunBundle["source"];
|
|
119
|
+
}
|
|
77
120
|
export interface RunScope {
|
|
121
|
+
/**
|
|
122
|
+
* Start a recorded route's run from its plan and input: the input's run id or one minted from
|
|
123
|
+
* `prefix`, the plan's mode, study and warnings, and an Observer that opens when the input says
|
|
124
|
+
* `open` and renders through the input's `deps.renderObserver`. Then capture the run's source.
|
|
125
|
+
* The run starts as the form below does.
|
|
126
|
+
*/
|
|
127
|
+
startRun(plan: StartRunPlan, input: StartRunInput, options: RouteRunOptions): Promise<{
|
|
128
|
+
ok: true;
|
|
129
|
+
run: RecordedRun;
|
|
130
|
+
} | RunIdInUse>;
|
|
78
131
|
/**
|
|
79
132
|
* Create `.humanish/runs/<id>` exclusively, begin status.json and wait for its first write, so
|
|
80
133
|
* a run that got past this point has a status record before any sandbox exists. An id whose
|
|
81
|
-
* directory exists is refused before any write. A scope starts at most one run.
|
|
134
|
+
* directory exists is refused before any write. A scope starts at most one run. Preview calls
|
|
135
|
+
* this form, since it builds its source before the run and mints its own id.
|
|
82
136
|
*/
|
|
83
137
|
startRun(options: StartRunOptions): Promise<{
|
|
84
138
|
ok: true;
|
package/dist/run/run.js
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
// token that lets the Observer and automatic analysis read it. Routes build bundles and write
|
|
3
3
|
// their own evidence files; this module owns when the run starts, how it is published, and how it
|
|
4
4
|
// is closed on every exit.
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
5
6
|
import { redactText } from "../evidence/redaction.js";
|
|
6
7
|
import { buildObserverData } from "../observer/data.js";
|
|
7
8
|
import { renderObserver } from "../observer/render.js";
|
|
8
|
-
import { RUN_BUNDLE_FILE, PUBLIC_TARGET_CWD, } from "./bundle.js";
|
|
9
|
+
import { RUN_BUNDLE_FILE, PUBLIC_TARGET_CWD, buildRunSource, } from "./bundle.js";
|
|
9
10
|
import { judgeExecution, } from "./judge.js";
|
|
10
11
|
import {} from "./results.js";
|
|
11
12
|
import { createRunArtifactPaths, validatePreparedRunArtifactPaths, } from "./paths.js";
|
|
12
13
|
import { registerActiveRun } from "./active-runs.js";
|
|
13
14
|
import { writeContainedOutputFile, writePreparedRunLatestPointer } from "./contained-output.js";
|
|
14
15
|
import { scrubRunSandboxIds, withPublicSandboxIds } from "./sandbox-ids.js";
|
|
16
|
+
import { RunSecrets } from "./secrets.js";
|
|
15
17
|
import { beginRunStatus, runStatusOutcome, } from "./status.js";
|
|
16
18
|
/** An entry point called in a state that does not admit it. */
|
|
17
19
|
class RunLifecycleError extends Error {
|
|
@@ -239,7 +241,7 @@ export async function runScope(fn) {
|
|
|
239
241
|
return operation;
|
|
240
242
|
};
|
|
241
243
|
const refuse = (message) => Promise.reject(new RunLifecycleError(message));
|
|
242
|
-
const
|
|
244
|
+
const startRunWith = (options) => {
|
|
243
245
|
if (closed)
|
|
244
246
|
return refuse("The run scope has closed.");
|
|
245
247
|
if (started)
|
|
@@ -277,6 +279,9 @@ export async function runScope(fn) {
|
|
|
277
279
|
return { ok: true, run: run.run };
|
|
278
280
|
})());
|
|
279
281
|
};
|
|
282
|
+
function startRun(...args) {
|
|
283
|
+
return args.length === 1 ? startRunWith(args[0]) : startRecordedRun(startRunWith, ...args);
|
|
284
|
+
}
|
|
280
285
|
const openRun = (options, runId, paths, runStatus) => {
|
|
281
286
|
const now = options.now ?? Date.now;
|
|
282
287
|
const createdAt = new Date(now()).toISOString();
|
|
@@ -301,6 +306,7 @@ export async function runScope(fn) {
|
|
|
301
306
|
mode: options.mode,
|
|
302
307
|
...(options.study === undefined ? {} : { study: options.study }),
|
|
303
308
|
paths,
|
|
309
|
+
secrets: options.secrets ?? new RunSecrets([]),
|
|
304
310
|
participantStarted() {
|
|
305
311
|
participantsRan = true;
|
|
306
312
|
},
|
|
@@ -348,3 +354,33 @@ export async function runScope(fn) {
|
|
|
348
354
|
}
|
|
349
355
|
return { result: outcome.result, finished };
|
|
350
356
|
}
|
|
357
|
+
/** Start a recorded route's run through `start`, the scope's own form, then capture its source. */
|
|
358
|
+
async function startRecordedRun(start, plan, input, route) {
|
|
359
|
+
const started = await start({
|
|
360
|
+
cwd: route.cwd,
|
|
361
|
+
runId: input.runId,
|
|
362
|
+
mintRunId: () => mintRunId(route.prefix),
|
|
363
|
+
mode: plan.dryRun ? "dry-run" : "live",
|
|
364
|
+
study: plan.study,
|
|
365
|
+
warnings: plan.warnings,
|
|
366
|
+
sandboxes: route.sandboxes,
|
|
367
|
+
renderReview: route.renderReview,
|
|
368
|
+
observer: { open: input.open === true, render: input.deps?.renderObserver },
|
|
369
|
+
now: route.now,
|
|
370
|
+
secrets: route.secrets,
|
|
371
|
+
});
|
|
372
|
+
if (!started.ok)
|
|
373
|
+
return started;
|
|
374
|
+
const source = await buildRunSource({
|
|
375
|
+
cwd: route.cwd,
|
|
376
|
+
capturedAt: started.run.createdAt,
|
|
377
|
+
humanishSource: "present",
|
|
378
|
+
packageName: "humanish",
|
|
379
|
+
});
|
|
380
|
+
return { ok: true, run: { ...started.run, source } };
|
|
381
|
+
}
|
|
382
|
+
/** `<prefix>-<wall-clock time, with : and . as ->-<8 hex>`. */
|
|
383
|
+
function mintRunId(prefix) {
|
|
384
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
385
|
+
return `${prefix}-${stamp}-${randomBytes(4).toString("hex")}`;
|
|
386
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Each route's seed list, marker and floor are its own; the defaults are what most routes use. */
|
|
2
|
+
interface RunSecretsOptions {
|
|
3
|
+
/** The text that replaces each value. */
|
|
4
|
+
readonly marker?: string;
|
|
5
|
+
/** The shortest value scrubbed. A shorter value is ordinary text often enough to leave alone. */
|
|
6
|
+
readonly minLength?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class RunSecrets {
|
|
9
|
+
#private;
|
|
10
|
+
/**
|
|
11
|
+
* Replaces every held value with the marker. It reads the values on each call, so a value added
|
|
12
|
+
* after the scrub was handed to a participant or a subject is scrubbed from then on.
|
|
13
|
+
*/
|
|
14
|
+
readonly scrub: (text: string) => string;
|
|
15
|
+
/** `seed` keeps its order; values shorter than the floor are dropped. */
|
|
16
|
+
constructor(seed: readonly string[], options?: RunSecretsOptions);
|
|
17
|
+
/** Holds each value the run learns while it runs, unless it is under the floor or already held. */
|
|
18
|
+
add(values: readonly string[]): void;
|
|
19
|
+
/** The held values, seed first, in one array that later `add` calls extend. */
|
|
20
|
+
values(): readonly string[];
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The literal values one run scrubs from what it writes: the provider keys and subject env values
|
|
2
|
+
// its route reads, and the values the run provisions while it runs, such as the addresses email
|
|
3
|
+
// receiving registers. These have no secret shape for pattern redaction to find, so each is
|
|
4
|
+
// replaced as written; redactText runs after the scrub and finds secrets by shape.
|
|
5
|
+
import { scrubLiterals } from "../evidence/redaction.js";
|
|
6
|
+
export class RunSecrets {
|
|
7
|
+
// Private fields stay out of JSON.stringify and object spread, so a Run that reaches a writer
|
|
8
|
+
// whole cannot carry the values with it.
|
|
9
|
+
#held;
|
|
10
|
+
#minLength;
|
|
11
|
+
/**
|
|
12
|
+
* Replaces every held value with the marker. It reads the values on each call, so a value added
|
|
13
|
+
* after the scrub was handed to a participant or a subject is scrubbed from then on.
|
|
14
|
+
*/
|
|
15
|
+
scrub;
|
|
16
|
+
/** `seed` keeps its order; values shorter than the floor are dropped. */
|
|
17
|
+
constructor(seed, options = {}) {
|
|
18
|
+
// An empty value would match between every character.
|
|
19
|
+
this.#minLength = Math.max(1, options.minLength ?? 4);
|
|
20
|
+
this.#held = seed.filter((value) => value.length >= this.#minLength);
|
|
21
|
+
this.scrub = scrubLiterals(this.#held, options.marker);
|
|
22
|
+
}
|
|
23
|
+
/** Holds each value the run learns while it runs, unless it is under the floor or already held. */
|
|
24
|
+
add(values) {
|
|
25
|
+
for (const value of values)
|
|
26
|
+
if (value.length >= this.#minLength && !this.#held.includes(value))
|
|
27
|
+
this.#held.push(value);
|
|
28
|
+
}
|
|
29
|
+
/** The held values, seed first, in one array that later `add` calls extend. */
|
|
30
|
+
values() {
|
|
31
|
+
return this.#held;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -7,5 +7,39 @@ export interface StudyResultIdentity<R extends string> {
|
|
|
7
7
|
/** The study's `id`. */
|
|
8
8
|
studyId: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* The error codes every recorded route's result can carry besides its own: a study, option or
|
|
12
|
+
* analysis refusal from before the route's checks, and a run id that is already taken. Each route's
|
|
13
|
+
* error-code union is this set plus the codes named for that route.
|
|
14
|
+
*/
|
|
15
|
+
export type StudyResultErrorCode = "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_RUN_ID_IN_USE";
|
|
10
16
|
/** The identity fields for a result of route `route` from the study `studyId`. */
|
|
11
17
|
export declare function studyResultIdentity<R extends string>(route: R, studyId: string): StudyResultIdentity<R>;
|
|
18
|
+
/** What every refused study's result carries besides its identity and its route's own fields. */
|
|
19
|
+
export interface RefusalBase<C extends string> {
|
|
20
|
+
/** The study's `id`. */
|
|
21
|
+
studyId: string;
|
|
22
|
+
cwd: string;
|
|
23
|
+
/** The caller's warnings array, kept by reference so warnings added later still show. Empty when omitted. */
|
|
24
|
+
warnings?: string[];
|
|
25
|
+
error: {
|
|
26
|
+
code: C;
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A refused study's result on route `route`: its identity, `ok: false` and `cwd`, then `fields`
|
|
32
|
+
* in the caller's order, then `warnings` and `error`. A result is written as JSON, so the key order
|
|
33
|
+
* is part of the output; a site whose order differs spreads its remaining fields after this. The
|
|
34
|
+
* caller keeps runId, actor, dryRun and the route's fields, whose values and order differ by site.
|
|
35
|
+
*/
|
|
36
|
+
export declare function refusedResult<R extends string, C extends string, F extends object>(route: R, base: RefusalBase<C>, fields: F): StudyResultIdentity<R> & {
|
|
37
|
+
ok: false;
|
|
38
|
+
cwd: string;
|
|
39
|
+
} & F & {
|
|
40
|
+
warnings: string[];
|
|
41
|
+
error: {
|
|
42
|
+
code: C;
|
|
43
|
+
message: string;
|
|
44
|
+
};
|
|
45
|
+
};
|
package/dist/run/study-result.js
CHANGED
|
@@ -7,3 +7,19 @@ export const STUDY_RESULT_SCHEMA = "humanish.study-result.v1";
|
|
|
7
7
|
export function studyResultIdentity(route, studyId) {
|
|
8
8
|
return { schema: STUDY_RESULT_SCHEMA, route, studyId };
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* A refused study's result on route `route`: its identity, `ok: false` and `cwd`, then `fields`
|
|
12
|
+
* in the caller's order, then `warnings` and `error`. A result is written as JSON, so the key order
|
|
13
|
+
* is part of the output; a site whose order differs spreads its remaining fields after this. The
|
|
14
|
+
* caller keeps runId, actor, dryRun and the route's fields, whose values and order differ by site.
|
|
15
|
+
*/
|
|
16
|
+
export function refusedResult(route, base, fields) {
|
|
17
|
+
return {
|
|
18
|
+
...studyResultIdentity(route, base.studyId),
|
|
19
|
+
ok: false,
|
|
20
|
+
cwd: base.cwd,
|
|
21
|
+
...fields,
|
|
22
|
+
warnings: base.warnings ?? [],
|
|
23
|
+
error: base.error,
|
|
24
|
+
};
|
|
25
|
+
}
|