@peac/cli 0.13.4 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/emit-lifecycle.d.ts +140 -0
- package/dist/commands/emit-lifecycle.d.ts.map +1 -0
- package/dist/commands/observe-command.d.ts +135 -0
- package/dist/commands/observe-command.d.ts.map +1 -0
- package/dist/commands/record-command.d.ts +102 -0
- package/dist/commands/record-command.d.ts.map +1 -0
- package/dist/index.cjs +1922 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1930 -10
- package/dist/index.mjs.map +1 -1
- package/dist/lib/capture.d.ts +118 -0
- package/dist/lib/capture.d.ts.map +1 -0
- package/dist/lib/cli-limits.d.ts +49 -0
- package/dist/lib/cli-limits.d.ts.map +1 -0
- package/dist/lib/command-option-validation.d.ts +94 -0
- package/dist/lib/command-option-validation.d.ts.map +1 -0
- package/dist/lib/issuer-key-loader.d.ts +56 -0
- package/dist/lib/issuer-key-loader.d.ts.map +1 -0
- package/dist/lib/observation-builder.d.ts +79 -0
- package/dist/lib/observation-builder.d.ts.map +1 -0
- package/dist/lib/observation-pipeline.d.ts +96 -0
- package/dist/lib/observation-pipeline.d.ts.map +1 -0
- package/dist/lib/output-preflight.d.ts +28 -0
- package/dist/lib/output-preflight.d.ts.map +1 -0
- package/dist/lib/secret-scan.d.ts +35 -0
- package/dist/lib/secret-scan.d.ts.map +1 -0
- package/package.json +8 -8
package/dist/index.mjs
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command, CommanderError } from 'commander';
|
|
3
|
-
import { verifyReceipt, fetchIssuerConfig, parseIssuerConfig } from '@peac/protocol';
|
|
4
|
-
import { generateKeypair, base64urlEncode, sign, decode, canonicalize } from '@peac/crypto';
|
|
2
|
+
import { Command, CommanderError, Option } from 'commander';
|
|
3
|
+
import { verifyReceipt, fetchIssuerConfig, parseIssuerConfig, issue, IssueError } from '@peac/protocol';
|
|
4
|
+
import { generateKeypair, base64urlEncode, sign, decode, canonicalize, sha256Hex as sha256Hex$1, validateKeypair, base64urlDecode, derivePublicKey } from '@peac/crypto';
|
|
5
5
|
import * as fs6 from 'fs';
|
|
6
|
-
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, statSync, openSync, constants, closeSync, unlinkSync, createReadStream } from 'fs';
|
|
7
7
|
import * as path6 from 'path';
|
|
8
|
-
import { join } from 'path';
|
|
8
|
+
import { join, resolve, dirname, delimiter } from 'path';
|
|
9
9
|
import { loadProfile, serializePolicyJson, serializePolicyYaml, ProfileError, createExamplePolicy, loadPolicy, PolicyValidationError, PolicyLoadError, compilePeacTxt, compileRobotsSnippet, compileAiprefTemplates, renderPolicyMarkdown, explainMatches, evaluate, listProfiles, getProfileSummary, parsePolicyDocument } from '@peac/policy-kit';
|
|
10
|
-
import { execSync } from 'child_process';
|
|
11
|
-
import { createHash } from 'crypto';
|
|
12
|
-
import { computeReceiptRef, validateWorkflowContextOrdered, isValidPurposeToken, isCanonicalPurpose, validateObligationsExtension, validateInteractionOrdered, validateDisputeAttestation, validateAttributionAttestation, validateAgentIdentityAttestation, ReceiptClaimsSchema, parseReceiptClaims } from '@peac/schema';
|
|
10
|
+
import { execSync, spawn } from 'child_process';
|
|
11
|
+
import { createHash, randomBytes } from 'crypto';
|
|
12
|
+
import { LIFECYCLE_OBSERVATION_TYPE_URIS, computeReceiptRef, CLI_EXECUTION_EXTENSION_KEY, CLI_COMMAND_EXECUTION_TYPE, validateLifecycleObservation, LIFECYCLE_OBSERVATION_EXTENSION_KEY, validateWorkflowContextOrdered, isValidPurposeToken, isCanonicalPurpose, validateObligationsExtension, validateInteractionOrdered, validateDisputeAttestation, validateAttributionAttestation, validateAgentIdentityAttestation, ReceiptClaimsSchema, CliExecutionSchema, isCanonicalIss, parseReceiptClaims, OpaqueRefSchema } from '@peac/schema';
|
|
13
13
|
import { POLICY } from '@peac/kernel';
|
|
14
14
|
import { readDisputeBundle } from '@peac/audit';
|
|
15
15
|
import { createRequire } from 'module';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
|
+
import { readFile } from 'fs/promises';
|
|
17
18
|
|
|
18
19
|
function getGlobalOptions(cmd) {
|
|
19
20
|
const parent = cmd.parent;
|
|
@@ -194,8 +195,8 @@ policy.command("validate").description("Validate a PEAC policy file (YAML or JSO
|
|
|
194
195
|
);
|
|
195
196
|
} else {
|
|
196
197
|
console.error("Policy validation failed:\n");
|
|
197
|
-
for (const
|
|
198
|
-
console.error(` - ${
|
|
198
|
+
for (const issue3 of err.issues) {
|
|
199
|
+
console.error(` - ${issue3.path.join(".")}: ${issue3.message}`);
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
process.exit(1);
|
|
@@ -2804,6 +2805,1922 @@ var DiscoverCommand = class {
|
|
|
2804
2805
|
}
|
|
2805
2806
|
};
|
|
2806
2807
|
|
|
2808
|
+
// src/lib/cli-limits.ts
|
|
2809
|
+
var CLI_LIMITS = {
|
|
2810
|
+
/** Default max bytes retained in the stdout sample buffer (raw mode only). */
|
|
2811
|
+
defaultStdoutSampleBytes: 16384,
|
|
2812
|
+
/** Default max bytes retained in the stderr sample buffer (raw mode only). */
|
|
2813
|
+
defaultStderrSampleBytes: 16384,
|
|
2814
|
+
/** Hard ceiling for the stdout / stderr sample buffer (raw mode only). */
|
|
2815
|
+
maxStdoutSampleBytes: 65536,
|
|
2816
|
+
maxStderrSampleBytes: 65536,
|
|
2817
|
+
/** Default max bytes recorded in argv (raw mode only). */
|
|
2818
|
+
defaultArgvCaptureBytes: 4096,
|
|
2819
|
+
/** Hard ceiling for argv bytes (raw mode only). */
|
|
2820
|
+
maxArgvCaptureBytes: 16384,
|
|
2821
|
+
/** Hard ceiling for env entry count (deny-by-default; allowlist <= this). */
|
|
2822
|
+
maxEnvEntries: 32,
|
|
2823
|
+
/** Default wrapper timeout (10 minutes). */
|
|
2824
|
+
defaultTimeoutMs: 6e5,
|
|
2825
|
+
/** Wrapper timeout hard ceiling (24 hours). */
|
|
2826
|
+
maxTimeoutMs: 864e5,
|
|
2827
|
+
/** Wrapper timeout floor (1 ms). */
|
|
2828
|
+
minTimeoutMs: 1,
|
|
2829
|
+
/** Default SIGTERM-to-SIGKILL grace (5 seconds). */
|
|
2830
|
+
defaultKillGraceMs: 5e3,
|
|
2831
|
+
/** SIGTERM-to-SIGKILL grace hard ceiling (60 seconds). */
|
|
2832
|
+
maxKillGraceMs: 6e4,
|
|
2833
|
+
/** SIGTERM-to-SIGKILL grace floor. */
|
|
2834
|
+
minKillGraceMs: 0
|
|
2835
|
+
};
|
|
2836
|
+
var SHELL_BINARY_BASENAMES = /* @__PURE__ */ new Set([
|
|
2837
|
+
"sh",
|
|
2838
|
+
"bash",
|
|
2839
|
+
"zsh",
|
|
2840
|
+
"dash",
|
|
2841
|
+
"fish",
|
|
2842
|
+
"pwsh",
|
|
2843
|
+
"cmd"
|
|
2844
|
+
]);
|
|
2845
|
+
function isShellBinary(programBasename) {
|
|
2846
|
+
return SHELL_BINARY_BASENAMES.has(programBasename.toLowerCase());
|
|
2847
|
+
}
|
|
2848
|
+
var CliSpawnFailedError = class extends Error {
|
|
2849
|
+
constructor(message, cause) {
|
|
2850
|
+
super(message);
|
|
2851
|
+
this.cause = cause;
|
|
2852
|
+
this.name = "CliSpawnFailedError";
|
|
2853
|
+
}
|
|
2854
|
+
code = "cli.spawn_failed";
|
|
2855
|
+
};
|
|
2856
|
+
function validateSpawnInputs(program2, args) {
|
|
2857
|
+
if (typeof program2 !== "string" || program2.length === 0) {
|
|
2858
|
+
throw new CliSpawnFailedError("program token must be a non-empty string");
|
|
2859
|
+
}
|
|
2860
|
+
if (program2.includes("\0")) {
|
|
2861
|
+
throw new CliSpawnFailedError("program token must not contain a NUL byte");
|
|
2862
|
+
}
|
|
2863
|
+
if (!Array.isArray(args)) {
|
|
2864
|
+
throw new CliSpawnFailedError("args must be an array of strings");
|
|
2865
|
+
}
|
|
2866
|
+
const validatedArgs = [];
|
|
2867
|
+
for (const a of args) {
|
|
2868
|
+
if (typeof a !== "string") {
|
|
2869
|
+
throw new CliSpawnFailedError("args must be an array of strings");
|
|
2870
|
+
}
|
|
2871
|
+
if (a.includes("\0")) {
|
|
2872
|
+
throw new CliSpawnFailedError("args must not contain NUL bytes");
|
|
2873
|
+
}
|
|
2874
|
+
validatedArgs.push(a);
|
|
2875
|
+
}
|
|
2876
|
+
return { program: program2, args: validatedArgs };
|
|
2877
|
+
}
|
|
2878
|
+
var SHA256_PREFIX = "sha256:";
|
|
2879
|
+
var SIGNAL_EXIT_CODES = {
|
|
2880
|
+
SIGHUP: 129,
|
|
2881
|
+
SIGINT: 130,
|
|
2882
|
+
SIGQUIT: 131,
|
|
2883
|
+
SIGABRT: 134,
|
|
2884
|
+
SIGKILL: 137,
|
|
2885
|
+
SIGUSR1: 138,
|
|
2886
|
+
SIGUSR2: 140,
|
|
2887
|
+
SIGPIPE: 141,
|
|
2888
|
+
SIGALRM: 142,
|
|
2889
|
+
SIGTERM: 143
|
|
2890
|
+
};
|
|
2891
|
+
function exitCodeForSignal(signal) {
|
|
2892
|
+
if (!signal) return 128;
|
|
2893
|
+
return SIGNAL_EXIT_CODES[signal] ?? 128;
|
|
2894
|
+
}
|
|
2895
|
+
function newStreamHasher() {
|
|
2896
|
+
return {
|
|
2897
|
+
hasher: createHash("sha256"),
|
|
2898
|
+
length: 0,
|
|
2899
|
+
truncated: false,
|
|
2900
|
+
sampleBuffer: [],
|
|
2901
|
+
sampleBytes: 0
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
function feedHasher(state, chunk, sampleCap, rawEnabled) {
|
|
2905
|
+
state.hasher.update(chunk);
|
|
2906
|
+
state.length += chunk.length;
|
|
2907
|
+
if (rawEnabled && state.sampleBytes < sampleCap) {
|
|
2908
|
+
const remaining = sampleCap - state.sampleBytes;
|
|
2909
|
+
if (chunk.length <= remaining) {
|
|
2910
|
+
state.sampleBuffer.push(chunk);
|
|
2911
|
+
state.sampleBytes += chunk.length;
|
|
2912
|
+
} else {
|
|
2913
|
+
state.sampleBuffer.push(chunk.subarray(0, remaining));
|
|
2914
|
+
state.sampleBytes += remaining;
|
|
2915
|
+
state.truncated = true;
|
|
2916
|
+
}
|
|
2917
|
+
} else if (rawEnabled && state.sampleBytes >= sampleCap) {
|
|
2918
|
+
state.truncated = true;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
function finalizeStreamRef(state, rawEnabled) {
|
|
2922
|
+
const ref = {
|
|
2923
|
+
length: state.length,
|
|
2924
|
+
sha256: SHA256_PREFIX + state.hasher.digest("hex"),
|
|
2925
|
+
truncated: state.truncated
|
|
2926
|
+
};
|
|
2927
|
+
if (rawEnabled && state.sampleBuffer.length > 0) {
|
|
2928
|
+
ref.sample_base64 = Buffer.concat(state.sampleBuffer, state.sampleBytes).toString("base64");
|
|
2929
|
+
}
|
|
2930
|
+
return ref;
|
|
2931
|
+
}
|
|
2932
|
+
async function pumpStdin(parent, childStdin, mode, abortSignal) {
|
|
2933
|
+
let length = 0;
|
|
2934
|
+
const truncated = false;
|
|
2935
|
+
const hasher = mode === "hashed" ? createHash("sha256") : null;
|
|
2936
|
+
return new Promise((resolve3) => {
|
|
2937
|
+
let settled = false;
|
|
2938
|
+
let drainWaiter = null;
|
|
2939
|
+
const finalize = () => {
|
|
2940
|
+
const out = { length, truncated };
|
|
2941
|
+
if (hasher) out.sha256 = SHA256_PREFIX + hasher.digest("hex");
|
|
2942
|
+
return out;
|
|
2943
|
+
};
|
|
2944
|
+
const cleanup = () => {
|
|
2945
|
+
parent.off("data", onData);
|
|
2946
|
+
parent.off("end", onEnd);
|
|
2947
|
+
parent.off("error", onParentError);
|
|
2948
|
+
childStdin.off("drain", onDrain);
|
|
2949
|
+
childStdin.off("error", onChildError);
|
|
2950
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
2951
|
+
};
|
|
2952
|
+
const settle = () => {
|
|
2953
|
+
if (settled) return;
|
|
2954
|
+
settled = true;
|
|
2955
|
+
cleanup();
|
|
2956
|
+
try {
|
|
2957
|
+
childStdin.end();
|
|
2958
|
+
} catch {
|
|
2959
|
+
}
|
|
2960
|
+
if (drainWaiter) {
|
|
2961
|
+
const reject = drainWaiter;
|
|
2962
|
+
drainWaiter = null;
|
|
2963
|
+
try {
|
|
2964
|
+
reject(new Error("pump-aborted"));
|
|
2965
|
+
} catch {
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
resolve3(finalize());
|
|
2969
|
+
};
|
|
2970
|
+
const onData = (chunk) => {
|
|
2971
|
+
if (settled) return;
|
|
2972
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2973
|
+
length += buf.length;
|
|
2974
|
+
if (hasher) hasher.update(buf);
|
|
2975
|
+
let writable = false;
|
|
2976
|
+
try {
|
|
2977
|
+
writable = childStdin.write(buf);
|
|
2978
|
+
} catch {
|
|
2979
|
+
settle();
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
if (!writable) {
|
|
2983
|
+
try {
|
|
2984
|
+
parent.pause();
|
|
2985
|
+
} catch {
|
|
2986
|
+
}
|
|
2987
|
+
const waiter = new Promise((res, rej) => {
|
|
2988
|
+
drainWaiter = rej;
|
|
2989
|
+
childStdin.once("drain", () => {
|
|
2990
|
+
drainWaiter = null;
|
|
2991
|
+
res();
|
|
2992
|
+
});
|
|
2993
|
+
});
|
|
2994
|
+
waiter.then(() => {
|
|
2995
|
+
if (settled) return;
|
|
2996
|
+
try {
|
|
2997
|
+
parent.resume();
|
|
2998
|
+
} catch {
|
|
2999
|
+
}
|
|
3000
|
+
}).catch(() => {
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
};
|
|
3004
|
+
const onEnd = () => settle();
|
|
3005
|
+
const onParentError = () => settle();
|
|
3006
|
+
const onChildError = () => settle();
|
|
3007
|
+
const onDrain = () => {
|
|
3008
|
+
};
|
|
3009
|
+
const onAbort = () => {
|
|
3010
|
+
try {
|
|
3011
|
+
parent.pause();
|
|
3012
|
+
} catch {
|
|
3013
|
+
}
|
|
3014
|
+
settle();
|
|
3015
|
+
};
|
|
3016
|
+
parent.on("data", onData);
|
|
3017
|
+
parent.on("end", onEnd);
|
|
3018
|
+
parent.on("error", onParentError);
|
|
3019
|
+
childStdin.on("drain", onDrain);
|
|
3020
|
+
childStdin.on("error", onChildError);
|
|
3021
|
+
if (abortSignal.aborted) {
|
|
3022
|
+
onAbort();
|
|
3023
|
+
} else {
|
|
3024
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
function nowIso() {
|
|
3029
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3030
|
+
}
|
|
3031
|
+
async function captureCommand(opts) {
|
|
3032
|
+
const startedAt = nowIso();
|
|
3033
|
+
const startMs = Date.now();
|
|
3034
|
+
const stdoutState = newStreamHasher();
|
|
3035
|
+
const stderrState = newStreamHasher();
|
|
3036
|
+
const stdioConfig = opts.stdinMode === "none" ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"];
|
|
3037
|
+
const { program: program2, args } = validateSpawnInputs(opts.program, opts.args);
|
|
3038
|
+
const child = spawn(program2, args, {
|
|
3039
|
+
cwd: opts.cwd,
|
|
3040
|
+
env: opts.env,
|
|
3041
|
+
stdio: stdioConfig,
|
|
3042
|
+
shell: false,
|
|
3043
|
+
windowsHide: true
|
|
3044
|
+
});
|
|
3045
|
+
child.stdout.on("data", (chunk) => {
|
|
3046
|
+
feedHasher(stdoutState, chunk, opts.stdoutSampleBytes, opts.rawCaptureEnabled);
|
|
3047
|
+
});
|
|
3048
|
+
child.stderr.on("data", (chunk) => {
|
|
3049
|
+
feedHasher(stderrState, chunk, opts.stderrSampleBytes, opts.rawCaptureEnabled);
|
|
3050
|
+
});
|
|
3051
|
+
let stdinCapture = {
|
|
3052
|
+
length: 0,
|
|
3053
|
+
truncated: false
|
|
3054
|
+
};
|
|
3055
|
+
let stdinPromise = null;
|
|
3056
|
+
const stdinAbort = new AbortController();
|
|
3057
|
+
if (opts.stdinMode !== "none") {
|
|
3058
|
+
const parent = opts.parentStdin ?? process.stdin;
|
|
3059
|
+
stdinPromise = pumpStdin(parent, child.stdin, opts.stdinMode, stdinAbort.signal);
|
|
3060
|
+
}
|
|
3061
|
+
let timedOut = false;
|
|
3062
|
+
let terminationSignal = null;
|
|
3063
|
+
let killHandle;
|
|
3064
|
+
const timeoutHandle = setTimeout(() => {
|
|
3065
|
+
timedOut = true;
|
|
3066
|
+
terminationSignal = "SIGTERM";
|
|
3067
|
+
try {
|
|
3068
|
+
child.kill("SIGTERM");
|
|
3069
|
+
} catch {
|
|
3070
|
+
}
|
|
3071
|
+
killHandle = setTimeout(() => {
|
|
3072
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
3073
|
+
terminationSignal = "SIGKILL";
|
|
3074
|
+
try {
|
|
3075
|
+
child.kill("SIGKILL");
|
|
3076
|
+
} catch {
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
}, opts.killGraceMs);
|
|
3080
|
+
}, opts.timeoutMs);
|
|
3081
|
+
const exitInfo = await new Promise(
|
|
3082
|
+
(resolve3, reject) => {
|
|
3083
|
+
let settled = false;
|
|
3084
|
+
child.on("close", (code, signal) => {
|
|
3085
|
+
if (settled) return;
|
|
3086
|
+
settled = true;
|
|
3087
|
+
clearTimeout(timeoutHandle);
|
|
3088
|
+
if (killHandle) clearTimeout(killHandle);
|
|
3089
|
+
stdinAbort.abort();
|
|
3090
|
+
const exitCode = code === null ? exitCodeForSignal(signal) : code;
|
|
3091
|
+
resolve3({ exitCode, signal: signal ?? null });
|
|
3092
|
+
});
|
|
3093
|
+
child.on("error", (err) => {
|
|
3094
|
+
if (settled) return;
|
|
3095
|
+
settled = true;
|
|
3096
|
+
clearTimeout(timeoutHandle);
|
|
3097
|
+
if (killHandle) clearTimeout(killHandle);
|
|
3098
|
+
stdinAbort.abort();
|
|
3099
|
+
reject(
|
|
3100
|
+
new CliSpawnFailedError(
|
|
3101
|
+
`failed to spawn child process: ${err instanceof Error ? err.message : String(err)}`,
|
|
3102
|
+
err
|
|
3103
|
+
)
|
|
3104
|
+
);
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
);
|
|
3108
|
+
if (stdinPromise) {
|
|
3109
|
+
stdinCapture = await stdinPromise;
|
|
3110
|
+
}
|
|
3111
|
+
const finishedAt = nowIso();
|
|
3112
|
+
const durationMs = Date.now() - startMs;
|
|
3113
|
+
const stdinRef = opts.stdinMode === "none" ? { mode: "none" } : opts.stdinMode === "length-only" ? { mode: "length-only", length: stdinCapture.length, truncated: stdinCapture.truncated } : {
|
|
3114
|
+
mode: "hashed",
|
|
3115
|
+
length: stdinCapture.length,
|
|
3116
|
+
sha256: stdinCapture.sha256,
|
|
3117
|
+
truncated: stdinCapture.truncated
|
|
3118
|
+
};
|
|
3119
|
+
return {
|
|
3120
|
+
exitCode: exitInfo.exitCode,
|
|
3121
|
+
signal: exitInfo.signal,
|
|
3122
|
+
timedOut,
|
|
3123
|
+
terminationSignal,
|
|
3124
|
+
startedAt,
|
|
3125
|
+
finishedAt,
|
|
3126
|
+
durationMs,
|
|
3127
|
+
stdout: finalizeStreamRef(stdoutState, opts.rawCaptureEnabled),
|
|
3128
|
+
stderr: finalizeStreamRef(stderrState, opts.rawCaptureEnabled),
|
|
3129
|
+
stdin: stdinRef
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
function preflightOutputWritable(output2) {
|
|
3133
|
+
if (output2 === "-" || output2 === "") return null;
|
|
3134
|
+
const absPath = resolve(output2);
|
|
3135
|
+
const parent = dirname(absPath);
|
|
3136
|
+
try {
|
|
3137
|
+
const st = statSync(parent);
|
|
3138
|
+
if (!st.isDirectory()) {
|
|
3139
|
+
return `parent path '${parent}' is not a directory`;
|
|
3140
|
+
}
|
|
3141
|
+
} catch (err) {
|
|
3142
|
+
return `parent directory '${parent}' does not exist (${err?.code ?? (err instanceof Error ? err.message : String(err))})`;
|
|
3143
|
+
}
|
|
3144
|
+
let existingFd;
|
|
3145
|
+
try {
|
|
3146
|
+
existingFd = openSync(absPath, constants.O_WRONLY | constants.O_APPEND);
|
|
3147
|
+
} catch (err) {
|
|
3148
|
+
const code = err?.code;
|
|
3149
|
+
if (code !== "ENOENT") {
|
|
3150
|
+
return `cannot open '${absPath}' for write (${code ?? (err instanceof Error ? err.message : String(err))})`;
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
if (existingFd !== void 0) {
|
|
3154
|
+
try {
|
|
3155
|
+
closeSync(existingFd);
|
|
3156
|
+
} catch {
|
|
3157
|
+
}
|
|
3158
|
+
return null;
|
|
3159
|
+
}
|
|
3160
|
+
const tempName = `.peac-preflight-${randomBytes(8).toString("hex")}.tmp`;
|
|
3161
|
+
const tempPath = join(parent, tempName);
|
|
3162
|
+
let fd;
|
|
3163
|
+
try {
|
|
3164
|
+
fd = openSync(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
|
|
3165
|
+
} catch (err) {
|
|
3166
|
+
return `parent directory '${parent}' is not writable (${err?.code ?? (err instanceof Error ? err.message : String(err))})`;
|
|
3167
|
+
} finally {
|
|
3168
|
+
if (fd !== void 0) {
|
|
3169
|
+
try {
|
|
3170
|
+
closeSync(fd);
|
|
3171
|
+
} catch {
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
try {
|
|
3176
|
+
unlinkSync(tempPath);
|
|
3177
|
+
} catch {
|
|
3178
|
+
}
|
|
3179
|
+
return null;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// src/lib/secret-scan.ts
|
|
3183
|
+
var SECRET_PATTERNS = [
|
|
3184
|
+
// JWS / JWT (three base64url segments separated by dots; first segment
|
|
3185
|
+
// starts with a base64-encoded `{"`).
|
|
3186
|
+
{
|
|
3187
|
+
category: "jwt",
|
|
3188
|
+
pattern: /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/
|
|
3189
|
+
},
|
|
3190
|
+
// Bearer token in HTTP-auth shape.
|
|
3191
|
+
{
|
|
3192
|
+
category: "bearer-token",
|
|
3193
|
+
pattern: /\bBearer\s+[A-Za-z0-9._~+/-]{16,}=*/i
|
|
3194
|
+
},
|
|
3195
|
+
// AWS access key (AKIA / ASIA prefix + 16 uppercase alnum).
|
|
3196
|
+
{
|
|
3197
|
+
category: "aws-access-key",
|
|
3198
|
+
pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/
|
|
3199
|
+
},
|
|
3200
|
+
// Generic API-key shape (sk_ / pk_ / api[-_]?key= prefix + >=20 alnum).
|
|
3201
|
+
{
|
|
3202
|
+
category: "api-key",
|
|
3203
|
+
pattern: /\b(?:sk|pk|sk-live|sk-test|pk-live|pk-test)[_-][A-Za-z0-9]{20,}/
|
|
3204
|
+
},
|
|
3205
|
+
// High-entropy token (>=32 contiguous base64url-ish chars). Conservative
|
|
3206
|
+
// length to reduce false positives on random hex IDs.
|
|
3207
|
+
{
|
|
3208
|
+
category: "generic-high-entropy",
|
|
3209
|
+
pattern: /[A-Za-z0-9_-]{32,}/
|
|
3210
|
+
}
|
|
3211
|
+
];
|
|
3212
|
+
function scanForSecrets(input) {
|
|
3213
|
+
const text = typeof input === "string" ? input : Buffer.from(input).toString("utf8");
|
|
3214
|
+
for (const { category, pattern } of SECRET_PATTERNS) {
|
|
3215
|
+
if (pattern.test(text)) {
|
|
3216
|
+
return { category };
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
return null;
|
|
3220
|
+
}
|
|
3221
|
+
function scanArgvElement(token) {
|
|
3222
|
+
return scanForSecrets(token);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
// src/lib/observation-builder.ts
|
|
3226
|
+
var CliObservationBuilderError = class extends Error {
|
|
3227
|
+
code;
|
|
3228
|
+
constructor(code, message) {
|
|
3229
|
+
super(message);
|
|
3230
|
+
this.name = "CliObservationBuilderError";
|
|
3231
|
+
this.code = code;
|
|
3232
|
+
}
|
|
3233
|
+
};
|
|
3234
|
+
function basenameAny(p) {
|
|
3235
|
+
return p.replace(/^.*[\\/]/, "");
|
|
3236
|
+
}
|
|
3237
|
+
var SHA256_PREFIX2 = "sha256:";
|
|
3238
|
+
function sha256Hex(input) {
|
|
3239
|
+
return SHA256_PREFIX2 + createHash("sha256").update(input).digest("hex");
|
|
3240
|
+
}
|
|
3241
|
+
async function sha256OfFile(path9) {
|
|
3242
|
+
return new Promise((resolve3, reject) => {
|
|
3243
|
+
const hasher = createHash("sha256");
|
|
3244
|
+
const stream = createReadStream(path9);
|
|
3245
|
+
stream.on("data", (chunk) => {
|
|
3246
|
+
hasher.update(chunk);
|
|
3247
|
+
});
|
|
3248
|
+
stream.on("end", () => {
|
|
3249
|
+
resolve3(SHA256_PREFIX2 + hasher.digest("hex"));
|
|
3250
|
+
});
|
|
3251
|
+
stream.on("error", reject);
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
function isStructuralArgvToken(token) {
|
|
3255
|
+
if (token === "--") return true;
|
|
3256
|
+
if (/^-[A-Za-z0-9?]$/.test(token)) return true;
|
|
3257
|
+
if (/^--[A-Za-z][A-Za-z0-9_-]*$/.test(token)) return true;
|
|
3258
|
+
return false;
|
|
3259
|
+
}
|
|
3260
|
+
function encodeArgv(rawArgv, mode, argvCaptureBytes, secretScanEnabled) {
|
|
3261
|
+
const tokenCount = rawArgv.length;
|
|
3262
|
+
if (mode === "hashed") {
|
|
3263
|
+
const canon = JSON.stringify(rawArgv);
|
|
3264
|
+
return { argv_sha256: sha256Hex(canon), argv_token_count: tokenCount };
|
|
3265
|
+
}
|
|
3266
|
+
if (mode === "redacted") {
|
|
3267
|
+
const out2 = rawArgv.map((tok) => {
|
|
3268
|
+
if (isStructuralArgvToken(tok)) return tok;
|
|
3269
|
+
const eq = tok.match(/^(--[A-Za-z][A-Za-z0-9_-]*=)(.*)$/);
|
|
3270
|
+
if (eq) {
|
|
3271
|
+
const value = eq[2];
|
|
3272
|
+
const byteLen2 = Buffer.byteLength(value, "utf8");
|
|
3273
|
+
return `${eq[1]}<redacted:${byteLen2}>`;
|
|
3274
|
+
}
|
|
3275
|
+
const byteLen = Buffer.byteLength(tok, "utf8");
|
|
3276
|
+
return `<redacted:${byteLen}>`;
|
|
3277
|
+
});
|
|
3278
|
+
return { argv: out2, argv_token_count: tokenCount };
|
|
3279
|
+
}
|
|
3280
|
+
const out = rawArgv.map((tok, idx) => {
|
|
3281
|
+
if (Buffer.byteLength(tok, "utf8") > argvCaptureBytes) {
|
|
3282
|
+
throw new CliObservationBuilderError(
|
|
3283
|
+
"cli.argv_token_too_long",
|
|
3284
|
+
`command.argv[${idx}] (${Buffer.byteLength(tok, "utf8")} bytes) exceeds argv_max_bytes (${argvCaptureBytes}); raise --capture-argv-bytes or use --capture-mode hashed/redacted`
|
|
3285
|
+
);
|
|
3286
|
+
}
|
|
3287
|
+
let scanResult = null;
|
|
3288
|
+
if (secretScanEnabled) {
|
|
3289
|
+
scanResult = scanArgvElement(tok);
|
|
3290
|
+
}
|
|
3291
|
+
if (scanResult) {
|
|
3292
|
+
return `<secret-suppressed:${scanResult.category}>`;
|
|
3293
|
+
}
|
|
3294
|
+
return tok;
|
|
3295
|
+
});
|
|
3296
|
+
return { argv: out, argv_token_count: tokenCount };
|
|
3297
|
+
}
|
|
3298
|
+
function buildEnvBlock(parentEnv, allowlist, mode, rawEnvEnabled) {
|
|
3299
|
+
if (mode === "raw" && !rawEnvEnabled) {
|
|
3300
|
+
throw new CliObservationBuilderError(
|
|
3301
|
+
"cli.env_mode_inconsistent",
|
|
3302
|
+
"env.mode=raw requires --unsafe-allow-raw-env; refusing to silently downgrade to hashed"
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
const entries = {};
|
|
3306
|
+
for (const key of allowlist) {
|
|
3307
|
+
const val = parentEnv[key];
|
|
3308
|
+
if (val === void 0) continue;
|
|
3309
|
+
if (mode === "hashed") {
|
|
3310
|
+
entries[key] = { value_sha256: sha256Hex(val) };
|
|
3311
|
+
} else {
|
|
3312
|
+
entries[key] = { value: val };
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
return { mode, entries };
|
|
3316
|
+
}
|
|
3317
|
+
function buildCwd(cwd, mode) {
|
|
3318
|
+
switch (mode) {
|
|
3319
|
+
case "none":
|
|
3320
|
+
return { cwd_mode: "none" };
|
|
3321
|
+
case "hashed":
|
|
3322
|
+
return { cwd_mode: "hashed", cwd_sha256: sha256Hex(cwd) };
|
|
3323
|
+
case "basename":
|
|
3324
|
+
return { cwd_mode: "basename", cwd_basename: basenameAny(cwd) };
|
|
3325
|
+
case "absolute":
|
|
3326
|
+
return { cwd_mode: "absolute", cwd_absolute: cwd };
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
async function buildBinary(resolvedProgram, pathMode, shellMode) {
|
|
3330
|
+
const built = { path_mode: pathMode };
|
|
3331
|
+
let isRegularFile = false;
|
|
3332
|
+
if (pathMode !== "none") {
|
|
3333
|
+
try {
|
|
3334
|
+
const st = statSync(resolvedProgram);
|
|
3335
|
+
built.size_bytes = st.size;
|
|
3336
|
+
built.mode_octal = (st.mode & 4095).toString(8).padStart(4, "0");
|
|
3337
|
+
isRegularFile = st.isFile();
|
|
3338
|
+
} catch {
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
switch (pathMode) {
|
|
3342
|
+
case "none":
|
|
3343
|
+
break;
|
|
3344
|
+
case "hashed":
|
|
3345
|
+
built.path_sha256 = sha256Hex(resolvedProgram);
|
|
3346
|
+
break;
|
|
3347
|
+
case "absolute":
|
|
3348
|
+
built.path_absolute = resolvedProgram;
|
|
3349
|
+
break;
|
|
3350
|
+
}
|
|
3351
|
+
if (pathMode !== "none" && isRegularFile) {
|
|
3352
|
+
try {
|
|
3353
|
+
built.sha256 = await sha256OfFile(resolvedProgram);
|
|
3354
|
+
} catch {
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
if (shellMode) {
|
|
3358
|
+
if (pathMode === "none") {
|
|
3359
|
+
throw new CliObservationBuilderError(
|
|
3360
|
+
"cli.shell_mode_required",
|
|
3361
|
+
"shell_mode=true requires --capture-binary-path != none so shell_ref has a defined source"
|
|
3362
|
+
);
|
|
3363
|
+
}
|
|
3364
|
+
built.shell_ref = built.path_sha256 ?? sha256Hex(resolvedProgram);
|
|
3365
|
+
}
|
|
3366
|
+
return built;
|
|
3367
|
+
}
|
|
3368
|
+
function applySecretScan(ref, scanEnabled) {
|
|
3369
|
+
if (!scanEnabled || ref.sample_base64 === void 0) {
|
|
3370
|
+
return ref;
|
|
3371
|
+
}
|
|
3372
|
+
const decoded = Buffer.from(ref.sample_base64, "base64").toString("utf8");
|
|
3373
|
+
const match = scanForSecrets(decoded);
|
|
3374
|
+
if (!match) return ref;
|
|
3375
|
+
const { sample_base64: _omit, ...rest } = ref;
|
|
3376
|
+
return {
|
|
3377
|
+
...rest,
|
|
3378
|
+
sample_suppressed_reason: "secret_pattern_detected",
|
|
3379
|
+
matched_pattern_category: match.category
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
async function buildObservation(input) {
|
|
3383
|
+
const argvEncoded = encodeArgv(
|
|
3384
|
+
input.rawArgv,
|
|
3385
|
+
input.argvMode,
|
|
3386
|
+
input.argvCaptureBytes,
|
|
3387
|
+
input.secretScanEnabled
|
|
3388
|
+
);
|
|
3389
|
+
const programName = basenameAny(input.programToken);
|
|
3390
|
+
const command = input.argvMode === "hashed" ? {
|
|
3391
|
+
program: programName,
|
|
3392
|
+
argv_mode: "hashed",
|
|
3393
|
+
argv_sha256: argvEncoded.argv_sha256,
|
|
3394
|
+
argv_token_count: argvEncoded.argv_token_count
|
|
3395
|
+
} : input.argvMode === "redacted" ? {
|
|
3396
|
+
program: programName,
|
|
3397
|
+
argv_mode: "redacted",
|
|
3398
|
+
argv: argvEncoded.argv,
|
|
3399
|
+
argv_token_count: argvEncoded.argv_token_count
|
|
3400
|
+
} : {
|
|
3401
|
+
program: programName,
|
|
3402
|
+
argv_mode: "raw",
|
|
3403
|
+
argv: argvEncoded.argv,
|
|
3404
|
+
argv_token_count: argvEncoded.argv_token_count
|
|
3405
|
+
};
|
|
3406
|
+
const cwd = buildCwd(input.cwd, input.cwdMode);
|
|
3407
|
+
const binary = await buildBinary(
|
|
3408
|
+
input.resolvedProgramPath,
|
|
3409
|
+
input.binaryPathMode,
|
|
3410
|
+
input.shellMode
|
|
3411
|
+
);
|
|
3412
|
+
const env = buildEnvBlock(
|
|
3413
|
+
input.parentEnv,
|
|
3414
|
+
input.envAllowlist,
|
|
3415
|
+
input.envMode,
|
|
3416
|
+
input.rawEnvEnabled
|
|
3417
|
+
);
|
|
3418
|
+
const stdoutRef = applySecretScan(input.capture.stdout, input.secretScanEnabled);
|
|
3419
|
+
const stderrRef = applySecretScan(input.capture.stderr, input.secretScanEnabled);
|
|
3420
|
+
const observation = {
|
|
3421
|
+
type: CLI_COMMAND_EXECUTION_TYPE,
|
|
3422
|
+
surface: { kind: "cli" },
|
|
3423
|
+
command,
|
|
3424
|
+
cwd,
|
|
3425
|
+
binary,
|
|
3426
|
+
stdin_ref: input.capture.stdin,
|
|
3427
|
+
stdout_ref: stdoutRef,
|
|
3428
|
+
stderr_ref: stderrRef,
|
|
3429
|
+
env,
|
|
3430
|
+
started_at: input.capture.startedAt,
|
|
3431
|
+
finished_at: input.capture.finishedAt,
|
|
3432
|
+
duration_ms: input.capture.durationMs,
|
|
3433
|
+
exit_code: input.capture.exitCode,
|
|
3434
|
+
...input.capture.signal ? { signal: input.capture.signal } : {},
|
|
3435
|
+
timed_out: input.capture.timedOut,
|
|
3436
|
+
timeout_ms: input.timeoutMs,
|
|
3437
|
+
kill_grace_ms: input.killGraceMs,
|
|
3438
|
+
...input.capture.terminationSignal ? { termination_signal: input.capture.terminationSignal } : {},
|
|
3439
|
+
exit_code_mode: input.exitCodeMode,
|
|
3440
|
+
shell_mode: input.shellMode,
|
|
3441
|
+
execution_mode: input.executionMode,
|
|
3442
|
+
capture_policy: {
|
|
3443
|
+
stdout_max_bytes: input.stdoutSampleBytes,
|
|
3444
|
+
stderr_max_bytes: input.stderrSampleBytes,
|
|
3445
|
+
argv_max_bytes: input.argvCaptureBytes,
|
|
3446
|
+
env_allowlist: input.envAllowlist,
|
|
3447
|
+
stdin_mode: input.stdinMode,
|
|
3448
|
+
cwd_mode: input.cwdMode,
|
|
3449
|
+
binary_path_mode: input.binaryPathMode,
|
|
3450
|
+
secret_scan: input.secretScanEnabled,
|
|
3451
|
+
raw_capture_unsafely_allowed: input.rawCaptureEnabled,
|
|
3452
|
+
raw_env_unsafely_allowed: input.rawEnvEnabled,
|
|
3453
|
+
secret_scan_disabled_unsafely: input.secretScanDisabledUnsafely,
|
|
3454
|
+
timeout_ms: input.timeoutMs,
|
|
3455
|
+
kill_grace_ms: input.killGraceMs,
|
|
3456
|
+
exit_code_mode: input.exitCodeMode
|
|
3457
|
+
},
|
|
3458
|
+
platform: {
|
|
3459
|
+
os: process.platform,
|
|
3460
|
+
arch: process.arch,
|
|
3461
|
+
peac_cli_version: input.peacCliVersion
|
|
3462
|
+
},
|
|
3463
|
+
...input.policyDigest ? { policy_digest: input.policyDigest } : {},
|
|
3464
|
+
...input.configDigest ? { config_digest: input.configDigest } : {},
|
|
3465
|
+
...input.approvalRef ? { approval_ref: input.approvalRef } : {}
|
|
3466
|
+
};
|
|
3467
|
+
return observation;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// src/lib/observation-pipeline.ts
|
|
3471
|
+
function resolveProgramPath(token, childEnv) {
|
|
3472
|
+
if (token.includes("/") || token.includes("\\")) {
|
|
3473
|
+
return resolve(token);
|
|
3474
|
+
}
|
|
3475
|
+
const PATH = childEnv.PATH ?? "";
|
|
3476
|
+
const dirs = PATH.split(delimiter);
|
|
3477
|
+
const exts = process.platform === "win32" ? (childEnv.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
3478
|
+
for (const dir of dirs) {
|
|
3479
|
+
if (!dir) continue;
|
|
3480
|
+
for (const ext of exts) {
|
|
3481
|
+
const candidate = join(dir, token + ext);
|
|
3482
|
+
try {
|
|
3483
|
+
const st = statSync(candidate);
|
|
3484
|
+
if (!st.isFile()) continue;
|
|
3485
|
+
if (process.platform !== "win32" && (st.mode & 73) === 0) continue;
|
|
3486
|
+
return candidate;
|
|
3487
|
+
} catch {
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
return token;
|
|
3492
|
+
}
|
|
3493
|
+
var preflightOutputWritable2 = preflightOutputWritable;
|
|
3494
|
+
async function runObservationCore(opts, programToken, args, io) {
|
|
3495
|
+
const resolvedProgramPath = resolveProgramPath(programToken, io.childEnv);
|
|
3496
|
+
let capture;
|
|
3497
|
+
try {
|
|
3498
|
+
capture = await captureCommand({
|
|
3499
|
+
program: resolvedProgramPath,
|
|
3500
|
+
args,
|
|
3501
|
+
cwd: io.cwd,
|
|
3502
|
+
env: io.childEnv,
|
|
3503
|
+
stdinMode: opts.captureStdinMode,
|
|
3504
|
+
rawCaptureEnabled: opts.captureMode === "raw" && opts.unsafeAllowRawCapture,
|
|
3505
|
+
stdoutSampleBytes: opts.captureStdoutBytes,
|
|
3506
|
+
stderrSampleBytes: opts.captureStderrBytes,
|
|
3507
|
+
timeoutMs: opts.timeoutMs,
|
|
3508
|
+
killGraceMs: opts.killGraceMs
|
|
3509
|
+
});
|
|
3510
|
+
} catch (err) {
|
|
3511
|
+
if (err instanceof CliSpawnFailedError) {
|
|
3512
|
+
return { ok: false, code: err.code, message: err.message };
|
|
3513
|
+
}
|
|
3514
|
+
return {
|
|
3515
|
+
ok: false,
|
|
3516
|
+
code: "cli.spawn_failed",
|
|
3517
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
let observation;
|
|
3521
|
+
try {
|
|
3522
|
+
observation = await buildObservation({
|
|
3523
|
+
capture,
|
|
3524
|
+
programToken,
|
|
3525
|
+
resolvedProgramPath,
|
|
3526
|
+
rawArgv: args,
|
|
3527
|
+
cwd: io.cwd,
|
|
3528
|
+
argvMode: opts.captureMode,
|
|
3529
|
+
cwdMode: opts.captureCwdMode,
|
|
3530
|
+
binaryPathMode: opts.captureBinaryPath,
|
|
3531
|
+
envMode: opts.envMode,
|
|
3532
|
+
stdinMode: opts.captureStdinMode,
|
|
3533
|
+
envAllowlist: opts.envAllow,
|
|
3534
|
+
parentEnv: io.captureEnv,
|
|
3535
|
+
rawCaptureEnabled: opts.captureMode === "raw" && opts.unsafeAllowRawCapture,
|
|
3536
|
+
rawEnvEnabled: opts.envMode === "raw" && opts.unsafeAllowRawEnv,
|
|
3537
|
+
secretScanEnabled: opts.secretScan,
|
|
3538
|
+
secretScanDisabledUnsafely: opts.unsafeDisableSecretScan,
|
|
3539
|
+
argvCaptureBytes: opts.captureArgvBytes,
|
|
3540
|
+
stdoutSampleBytes: opts.captureStdoutBytes,
|
|
3541
|
+
stderrSampleBytes: opts.captureStderrBytes,
|
|
3542
|
+
timeoutMs: opts.timeoutMs,
|
|
3543
|
+
killGraceMs: opts.killGraceMs,
|
|
3544
|
+
exitCodeMode: opts.exitCodeMode,
|
|
3545
|
+
executionMode: opts.executionMode,
|
|
3546
|
+
shellMode: opts.shellMode,
|
|
3547
|
+
policyDigest: opts.policyDigest,
|
|
3548
|
+
configDigest: opts.configDigest,
|
|
3549
|
+
approvalRef: opts.approvalRef,
|
|
3550
|
+
peacCliVersion: io.peacCliVersion
|
|
3551
|
+
});
|
|
3552
|
+
} catch (err) {
|
|
3553
|
+
if (err instanceof CliObservationBuilderError) {
|
|
3554
|
+
return { ok: false, code: err.code, message: err.message };
|
|
3555
|
+
}
|
|
3556
|
+
return {
|
|
3557
|
+
ok: false,
|
|
3558
|
+
code: "cli.builder_failed",
|
|
3559
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
const parsed = CliExecutionSchema.safeParse(observation);
|
|
3563
|
+
if (!parsed.success) {
|
|
3564
|
+
const first = parsed.error.issues[0];
|
|
3565
|
+
const path9 = first?.path?.join(".") ?? "";
|
|
3566
|
+
return {
|
|
3567
|
+
ok: false,
|
|
3568
|
+
code: "cli.schema_rejection",
|
|
3569
|
+
message: `${path9 ? "[" + path9 + "] " : ""}${first?.message ?? "schema validation failed"}`
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
return { ok: true, observation: parsed.data, capture };
|
|
3573
|
+
}
|
|
3574
|
+
var COMMAND_OPTION_ERROR_CODES = {
|
|
3575
|
+
programRequired: "cli.program_required",
|
|
3576
|
+
unsafeFlagRequired: "cli.unsafe_flag_required",
|
|
3577
|
+
secretScanDisableRequiresUnsafeFlag: "cli.secret_scan_disable_requires_unsafe_flag",
|
|
3578
|
+
shellModeRequired: "cli.shell_mode_required",
|
|
3579
|
+
argvTokenTooLong: "cli.argv_token_too_long",
|
|
3580
|
+
outOfRange: "cli.out_of_range",
|
|
3581
|
+
invalidPolicyDigest: "cli.invalid_policy_digest",
|
|
3582
|
+
invalidConfigDigest: "cli.invalid_config_digest",
|
|
3583
|
+
invalidApprovalRef: "cli.invalid_approval_ref",
|
|
3584
|
+
invalidEnvKey: "cli.invalid_env_key",
|
|
3585
|
+
// Signing-only (used by validateSigningOptions).
|
|
3586
|
+
signingInputRequired: "cli.signing_input_required",
|
|
3587
|
+
signingInputConflict: "cli.signing_input_conflict",
|
|
3588
|
+
issuerIdRequired: "cli.issuer_id_required",
|
|
3589
|
+
issuerIdInvalid: "cli.issuer_id_invalid"
|
|
3590
|
+
};
|
|
3591
|
+
var SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
3592
|
+
var ENV_KEY_GRAMMAR = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3593
|
+
var ENV_KEY_BYTES_MAX = 256;
|
|
3594
|
+
function isValidIntInRange(n, min, max) {
|
|
3595
|
+
return typeof n === "number" && Number.isInteger(n) && Number.isFinite(n) && n >= min && n <= max;
|
|
3596
|
+
}
|
|
3597
|
+
function utf8Bytes(s) {
|
|
3598
|
+
return new TextEncoder().encode(s).byteLength;
|
|
3599
|
+
}
|
|
3600
|
+
function basenameAny2(p) {
|
|
3601
|
+
return p.replace(/^.*[\\/]/, "");
|
|
3602
|
+
}
|
|
3603
|
+
function validateCoreCommandOptions(opts, childArgv) {
|
|
3604
|
+
const failures = [];
|
|
3605
|
+
if (childArgv.length === 0) {
|
|
3606
|
+
failures.push({
|
|
3607
|
+
code: COMMAND_OPTION_ERROR_CODES.programRequired,
|
|
3608
|
+
message: "a program after `--` is required (e.g., -- echo hi)"
|
|
3609
|
+
});
|
|
3610
|
+
return failures;
|
|
3611
|
+
}
|
|
3612
|
+
if (!isValidIntInRange(opts.timeoutMs, CLI_LIMITS.minTimeoutMs, CLI_LIMITS.maxTimeoutMs)) {
|
|
3613
|
+
failures.push({
|
|
3614
|
+
code: COMMAND_OPTION_ERROR_CODES.outOfRange,
|
|
3615
|
+
message: `--timeout-ms must be a finite integer in [${CLI_LIMITS.minTimeoutMs}, ${CLI_LIMITS.maxTimeoutMs}]`
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
if (!isValidIntInRange(opts.killGraceMs, CLI_LIMITS.minKillGraceMs, CLI_LIMITS.maxKillGraceMs)) {
|
|
3619
|
+
failures.push({
|
|
3620
|
+
code: COMMAND_OPTION_ERROR_CODES.outOfRange,
|
|
3621
|
+
message: `--kill-grace-ms must be a finite integer in [${CLI_LIMITS.minKillGraceMs}, ${CLI_LIMITS.maxKillGraceMs}]`
|
|
3622
|
+
});
|
|
3623
|
+
}
|
|
3624
|
+
if (!isValidIntInRange(opts.captureStdoutBytes, 0, CLI_LIMITS.maxStdoutSampleBytes)) {
|
|
3625
|
+
failures.push({
|
|
3626
|
+
code: COMMAND_OPTION_ERROR_CODES.outOfRange,
|
|
3627
|
+
message: `--capture-stdout-bytes must be a finite integer in [0, ${CLI_LIMITS.maxStdoutSampleBytes}]`
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3630
|
+
if (!isValidIntInRange(opts.captureStderrBytes, 0, CLI_LIMITS.maxStderrSampleBytes)) {
|
|
3631
|
+
failures.push({
|
|
3632
|
+
code: COMMAND_OPTION_ERROR_CODES.outOfRange,
|
|
3633
|
+
message: `--capture-stderr-bytes must be a finite integer in [0, ${CLI_LIMITS.maxStderrSampleBytes}]`
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
3636
|
+
if (!isValidIntInRange(opts.captureArgvBytes, 0, CLI_LIMITS.maxArgvCaptureBytes)) {
|
|
3637
|
+
failures.push({
|
|
3638
|
+
code: COMMAND_OPTION_ERROR_CODES.outOfRange,
|
|
3639
|
+
message: `--capture-argv-bytes must be a finite integer in [0, ${CLI_LIMITS.maxArgvCaptureBytes}]`
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
if (opts.envAllow.length > CLI_LIMITS.maxEnvEntries) {
|
|
3643
|
+
failures.push({
|
|
3644
|
+
code: COMMAND_OPTION_ERROR_CODES.outOfRange,
|
|
3645
|
+
message: `--env-allow accepts at most ${CLI_LIMITS.maxEnvEntries} keys`
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
for (const key of opts.envAllow) {
|
|
3649
|
+
if (!ENV_KEY_GRAMMAR.test(key)) {
|
|
3650
|
+
failures.push({
|
|
3651
|
+
code: COMMAND_OPTION_ERROR_CODES.invalidEnvKey,
|
|
3652
|
+
message: `--env-allow key '${key}' must match ${ENV_KEY_GRAMMAR}`
|
|
3653
|
+
});
|
|
3654
|
+
} else if (utf8Bytes(key) > ENV_KEY_BYTES_MAX) {
|
|
3655
|
+
failures.push({
|
|
3656
|
+
code: COMMAND_OPTION_ERROR_CODES.invalidEnvKey,
|
|
3657
|
+
message: `--env-allow key '${key}' exceeds ${ENV_KEY_BYTES_MAX} UTF-8 bytes`
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
if (opts.policyDigest !== void 0 && !SHA256_DIGEST.test(opts.policyDigest)) {
|
|
3662
|
+
failures.push({
|
|
3663
|
+
code: COMMAND_OPTION_ERROR_CODES.invalidPolicyDigest,
|
|
3664
|
+
message: "--policy-digest must match sha256:<64 lowercase hex>"
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
if (opts.configDigest !== void 0 && !SHA256_DIGEST.test(opts.configDigest)) {
|
|
3668
|
+
failures.push({
|
|
3669
|
+
code: COMMAND_OPTION_ERROR_CODES.invalidConfigDigest,
|
|
3670
|
+
message: "--config-digest must match sha256:<64 lowercase hex>"
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
if (opts.approvalRef !== void 0) {
|
|
3674
|
+
const ref = OpaqueRefSchema.safeParse(opts.approvalRef);
|
|
3675
|
+
if (!ref.success) {
|
|
3676
|
+
failures.push({
|
|
3677
|
+
code: COMMAND_OPTION_ERROR_CODES.invalidApprovalRef,
|
|
3678
|
+
message: `--approval-ref must satisfy the opaque-reference grammar: ${ref.error.issues.map((i) => i.message).join("; ")}`
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
if (opts.captureMode === "raw" && !opts.unsafeAllowRawCapture) {
|
|
3683
|
+
failures.push({
|
|
3684
|
+
code: COMMAND_OPTION_ERROR_CODES.unsafeFlagRequired,
|
|
3685
|
+
message: "--capture-mode raw requires --unsafe-allow-raw-capture"
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
if (opts.envMode === "raw" && !opts.unsafeAllowRawEnv) {
|
|
3689
|
+
failures.push({
|
|
3690
|
+
code: COMMAND_OPTION_ERROR_CODES.unsafeFlagRequired,
|
|
3691
|
+
message: "--env-mode raw requires --unsafe-allow-raw-env"
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
if (opts.unsafeAllowRawCapture && !opts.secretScan && !opts.unsafeDisableSecretScan) {
|
|
3695
|
+
failures.push({
|
|
3696
|
+
code: COMMAND_OPTION_ERROR_CODES.secretScanDisableRequiresUnsafeFlag,
|
|
3697
|
+
message: "--secret-scan off under raw capture requires --unsafe-disable-secret-scan"
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
if (opts.captureMode === "raw" && opts.unsafeAllowRawCapture) {
|
|
3701
|
+
const tokens = childArgv.slice(1);
|
|
3702
|
+
for (const tok of tokens) {
|
|
3703
|
+
if (utf8Bytes(tok) > opts.captureArgvBytes) {
|
|
3704
|
+
failures.push({
|
|
3705
|
+
code: COMMAND_OPTION_ERROR_CODES.argvTokenTooLong,
|
|
3706
|
+
message: `raw argv token (${utf8Bytes(tok)} bytes) exceeds --capture-argv-bytes (${opts.captureArgvBytes})`
|
|
3707
|
+
});
|
|
3708
|
+
break;
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
const programToken = childArgv[0];
|
|
3713
|
+
const programBasename = basenameAny2(programToken);
|
|
3714
|
+
if (isShellBinary(programBasename) && !opts.shellMode) {
|
|
3715
|
+
failures.push({
|
|
3716
|
+
code: COMMAND_OPTION_ERROR_CODES.shellModeRequired,
|
|
3717
|
+
message: `program '${programBasename}' is a shell binary; --shell-mode is required to acknowledge that the user intentionally invoked a shell`
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
if (opts.shellMode && opts.captureBinaryPath === "none") {
|
|
3721
|
+
failures.push({
|
|
3722
|
+
code: COMMAND_OPTION_ERROR_CODES.shellModeRequired,
|
|
3723
|
+
message: "--shell-mode requires --capture-binary-path != none"
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
return failures;
|
|
3727
|
+
}
|
|
3728
|
+
function validateSigningOptions(opts) {
|
|
3729
|
+
const failures = [];
|
|
3730
|
+
const hasIssuerKey = typeof opts.issuerKey === "string" && opts.issuerKey.length > 0;
|
|
3731
|
+
const hasEphemeral = opts.unsafeEphemeralKey === true;
|
|
3732
|
+
if (!hasIssuerKey && !hasEphemeral) {
|
|
3733
|
+
failures.push({
|
|
3734
|
+
code: COMMAND_OPTION_ERROR_CODES.signingInputRequired,
|
|
3735
|
+
message: "record command requires exactly one of --issuer-key <env:VAR|file:/path> or --unsafe-ephemeral-key"
|
|
3736
|
+
});
|
|
3737
|
+
}
|
|
3738
|
+
if (hasIssuerKey && hasEphemeral) {
|
|
3739
|
+
failures.push({
|
|
3740
|
+
code: COMMAND_OPTION_ERROR_CODES.signingInputConflict,
|
|
3741
|
+
message: "--issuer-key and --unsafe-ephemeral-key are mutually exclusive"
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
if (typeof opts.issuerId !== "string" || opts.issuerId.length === 0) {
|
|
3745
|
+
failures.push({
|
|
3746
|
+
code: COMMAND_OPTION_ERROR_CODES.issuerIdRequired,
|
|
3747
|
+
message: "--issuer-id <url> is required (canonical issuer URL)"
|
|
3748
|
+
});
|
|
3749
|
+
} else if (!isCanonicalIss(opts.issuerId)) {
|
|
3750
|
+
failures.push({
|
|
3751
|
+
code: COMMAND_OPTION_ERROR_CODES.issuerIdInvalid,
|
|
3752
|
+
message: `--issuer-id '${opts.issuerId}' is not canonical (expected https:// ASCII origin or did: identifier)`
|
|
3753
|
+
});
|
|
3754
|
+
}
|
|
3755
|
+
return failures;
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
// src/commands/observe-command.ts
|
|
3759
|
+
var ARGV_MODES = ["hashed", "redacted", "raw"];
|
|
3760
|
+
var CWD_MODES = ["none", "hashed", "basename", "absolute"];
|
|
3761
|
+
var BINARY_PATH_MODES = ["none", "hashed", "absolute"];
|
|
3762
|
+
var STDIN_MODES = ["none", "length-only", "hashed"];
|
|
3763
|
+
var ENV_MODES = ["hashed", "raw"];
|
|
3764
|
+
var EXIT_CODE_MODES = ["child", "record"];
|
|
3765
|
+
var EXECUTION_MODES = [
|
|
3766
|
+
"deterministic_script",
|
|
3767
|
+
"templated_flow",
|
|
3768
|
+
"agent_loop",
|
|
3769
|
+
"human_step",
|
|
3770
|
+
"hybrid"
|
|
3771
|
+
];
|
|
3772
|
+
var OBSERVE_COMMAND_ERROR_CODES = {
|
|
3773
|
+
outputWriteFailed: "cli.output_write_failed"
|
|
3774
|
+
};
|
|
3775
|
+
var DEFAULT_OPTIONS = {
|
|
3776
|
+
captureMode: "hashed",
|
|
3777
|
+
unsafeAllowRawCapture: false,
|
|
3778
|
+
captureStdinMode: "none",
|
|
3779
|
+
captureStdoutBytes: CLI_LIMITS.defaultStdoutSampleBytes,
|
|
3780
|
+
captureStderrBytes: CLI_LIMITS.defaultStderrSampleBytes,
|
|
3781
|
+
captureArgvBytes: CLI_LIMITS.defaultArgvCaptureBytes,
|
|
3782
|
+
envAllow: [],
|
|
3783
|
+
envMode: "hashed",
|
|
3784
|
+
unsafeAllowRawEnv: false,
|
|
3785
|
+
captureCwdMode: "hashed",
|
|
3786
|
+
captureBinaryPath: "hashed",
|
|
3787
|
+
secretScan: true,
|
|
3788
|
+
unsafeDisableSecretScan: false,
|
|
3789
|
+
executionMode: "deterministic_script",
|
|
3790
|
+
shellMode: false,
|
|
3791
|
+
output: "-",
|
|
3792
|
+
timeoutMs: CLI_LIMITS.defaultTimeoutMs,
|
|
3793
|
+
killGraceMs: CLI_LIMITS.defaultKillGraceMs,
|
|
3794
|
+
exitCodeMode: "child"
|
|
3795
|
+
};
|
|
3796
|
+
function parseIntegerFlag(raw) {
|
|
3797
|
+
if (raw.trim() === "") return Number.NaN;
|
|
3798
|
+
const n = Number(raw);
|
|
3799
|
+
return Number.isInteger(n) && Number.isFinite(n) ? n : Number.NaN;
|
|
3800
|
+
}
|
|
3801
|
+
function validateObserveOptions(opts, childArgv) {
|
|
3802
|
+
return validateCoreCommandOptions(opts, childArgv);
|
|
3803
|
+
}
|
|
3804
|
+
async function runObserveCommand(options, childArgv, io = {}) {
|
|
3805
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
3806
|
+
const writeStdout = io.writeStdout ?? ((c) => process.stdout.write(c));
|
|
3807
|
+
const writeStderr = io.writeStderr ?? ((c) => process.stderr.write(c));
|
|
3808
|
+
const childEnv = io.childEnv ?? process.env;
|
|
3809
|
+
const captureEnv = io.captureEnv ?? process.env;
|
|
3810
|
+
const cwd = io.cwd ?? process.cwd();
|
|
3811
|
+
const peacCliVersion = io.peacCliVersion ?? "0.14.1";
|
|
3812
|
+
const failures = validateObserveOptions(opts, childArgv);
|
|
3813
|
+
if (failures.length > 0) {
|
|
3814
|
+
for (const f of failures) {
|
|
3815
|
+
writeStderr(`${f.code}: ${f.message}
|
|
3816
|
+
`);
|
|
3817
|
+
}
|
|
3818
|
+
return { exitCode: 2 };
|
|
3819
|
+
}
|
|
3820
|
+
const programToken = childArgv[0];
|
|
3821
|
+
const args = childArgv.slice(1);
|
|
3822
|
+
const outputErr = preflightOutputWritable2(opts.output);
|
|
3823
|
+
if (outputErr !== null) {
|
|
3824
|
+
writeStderr(`${OBSERVE_COMMAND_ERROR_CODES.outputWriteFailed}: ${outputErr}
|
|
3825
|
+
`);
|
|
3826
|
+
return { exitCode: 2 };
|
|
3827
|
+
}
|
|
3828
|
+
const core = await runObservationCore(opts, programToken, args, {
|
|
3829
|
+
childEnv,
|
|
3830
|
+
captureEnv,
|
|
3831
|
+
cwd,
|
|
3832
|
+
peacCliVersion
|
|
3833
|
+
});
|
|
3834
|
+
if (!core.ok) {
|
|
3835
|
+
writeStderr(`${core.code}: ${core.message}
|
|
3836
|
+
`);
|
|
3837
|
+
return { exitCode: 2 };
|
|
3838
|
+
}
|
|
3839
|
+
const json = JSON.stringify(core.observation);
|
|
3840
|
+
if (opts.output === "-" || opts.output === "") {
|
|
3841
|
+
writeStdout(json + "\n");
|
|
3842
|
+
} else {
|
|
3843
|
+
try {
|
|
3844
|
+
writeFileSync(opts.output, json + "\n");
|
|
3845
|
+
} catch (err) {
|
|
3846
|
+
writeStderr(
|
|
3847
|
+
`${OBSERVE_COMMAND_ERROR_CODES.outputWriteFailed}: failed to write '${opts.output}': ${err instanceof Error ? err.message : String(err)}
|
|
3848
|
+
`
|
|
3849
|
+
);
|
|
3850
|
+
return { exitCode: 2 };
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
if (opts.exitCodeMode === "record") {
|
|
3854
|
+
return { exitCode: 0 };
|
|
3855
|
+
}
|
|
3856
|
+
return { exitCode: core.capture.exitCode };
|
|
3857
|
+
}
|
|
3858
|
+
function observeCommandSubcommand() {
|
|
3859
|
+
const cmd = new Command("command");
|
|
3860
|
+
cmd.description(
|
|
3861
|
+
"Wrap a command and emit an unsigned CLI execution observation record. observe command is an OBSERVER, not a sandbox, permission system, shell orchestrator, or process supervisor. The command after `--` is spawned exactly as supplied (shell: false)."
|
|
3862
|
+
).addOption(
|
|
3863
|
+
new Option("--capture-mode <mode>", "argv capture mode").choices([...ARGV_MODES]).default("hashed")
|
|
3864
|
+
).option(
|
|
3865
|
+
"--unsafe-allow-raw-capture",
|
|
3866
|
+
"required alongside --capture-mode raw to acknowledge raw stream sample emission",
|
|
3867
|
+
false
|
|
3868
|
+
).addOption(
|
|
3869
|
+
new Option("--capture-stdin-mode <mode>", "stdin capture (and pass-through) mode").choices([...STDIN_MODES]).default("none")
|
|
3870
|
+
).option(
|
|
3871
|
+
"--capture-stdout-bytes <n>",
|
|
3872
|
+
`stdout sample cap (raw mode only); max ${CLI_LIMITS.maxStdoutSampleBytes}`,
|
|
3873
|
+
parseIntegerFlag,
|
|
3874
|
+
CLI_LIMITS.defaultStdoutSampleBytes
|
|
3875
|
+
).option(
|
|
3876
|
+
"--capture-stderr-bytes <n>",
|
|
3877
|
+
`stderr sample cap (raw mode only); max ${CLI_LIMITS.maxStderrSampleBytes}`,
|
|
3878
|
+
parseIntegerFlag,
|
|
3879
|
+
CLI_LIMITS.defaultStderrSampleBytes
|
|
3880
|
+
).option(
|
|
3881
|
+
"--capture-argv-bytes <n>",
|
|
3882
|
+
`argv per-token byte cap (raw mode only); max ${CLI_LIMITS.maxArgvCaptureBytes}`,
|
|
3883
|
+
parseIntegerFlag,
|
|
3884
|
+
CLI_LIMITS.defaultArgvCaptureBytes
|
|
3885
|
+
).option(
|
|
3886
|
+
"--env-allow <KEYS>",
|
|
3887
|
+
"comma-separated allowlist of env vars to record (deny-by-default)",
|
|
3888
|
+
(v, prev) => prev.concat(
|
|
3889
|
+
v.split(",").map((s) => s.trim()).filter(Boolean)
|
|
3890
|
+
),
|
|
3891
|
+
[]
|
|
3892
|
+
).addOption(
|
|
3893
|
+
new Option("--env-mode <mode>", "env value capture mode").choices([...ENV_MODES]).default("hashed")
|
|
3894
|
+
).option(
|
|
3895
|
+
"--unsafe-allow-raw-env",
|
|
3896
|
+
"required alongside --env-mode raw to acknowledge raw env value capture",
|
|
3897
|
+
false
|
|
3898
|
+
).addOption(
|
|
3899
|
+
new Option("--capture-cwd-mode <mode>", "cwd capture mode").choices([...CWD_MODES]).default("hashed")
|
|
3900
|
+
).addOption(
|
|
3901
|
+
new Option("--capture-binary-path <mode>", "binary path capture mode").choices([...BINARY_PATH_MODES]).default("hashed")
|
|
3902
|
+
).addOption(
|
|
3903
|
+
new Option("--secret-scan <state>", "secret-scan over raw samples").choices(["on", "off"]).default("on")
|
|
3904
|
+
).option(
|
|
3905
|
+
"--unsafe-disable-secret-scan",
|
|
3906
|
+
"required to disable secret-scan when raw capture is enabled",
|
|
3907
|
+
false
|
|
3908
|
+
).option("--policy-digest <sha256>", "optional policy digest (sha256:<hex>)").option("--config-digest <sha256>", "optional config digest (sha256:<hex>)").option("--approval-ref <opaque>", "optional opaque approval reference").addOption(
|
|
3909
|
+
new Option("--execution-mode <mode>", "execution-mode tag for this record").choices([...EXECUTION_MODES]).default("deterministic_script")
|
|
3910
|
+
).option(
|
|
3911
|
+
"--shell-mode",
|
|
3912
|
+
"acknowledge that the program is a shell binary; PEAC does NOT rewrite the command",
|
|
3913
|
+
false
|
|
3914
|
+
).option("--output <file>", "output path for the JSON record (default: stdout)", "-").option(
|
|
3915
|
+
"--timeout-ms <n>",
|
|
3916
|
+
`wrapper timeout in ms; max ${CLI_LIMITS.maxTimeoutMs} (24h)`,
|
|
3917
|
+
parseIntegerFlag,
|
|
3918
|
+
CLI_LIMITS.defaultTimeoutMs
|
|
3919
|
+
).option(
|
|
3920
|
+
"--kill-grace-ms <n>",
|
|
3921
|
+
`SIGTERM-to-SIGKILL grace in ms; max ${CLI_LIMITS.maxKillGraceMs}`,
|
|
3922
|
+
parseIntegerFlag,
|
|
3923
|
+
CLI_LIMITS.defaultKillGraceMs
|
|
3924
|
+
).addOption(
|
|
3925
|
+
new Option("--exit-code-mode <mode>", "wrapper exit-code policy").choices([...EXIT_CODE_MODES]).default("child")
|
|
3926
|
+
).allowUnknownOption(false).allowExcessArguments(true);
|
|
3927
|
+
cmd.action(async (rawOpts, cmdInstance) => {
|
|
3928
|
+
const childArgv = cmdInstance.args;
|
|
3929
|
+
const options = {
|
|
3930
|
+
captureMode: rawOpts.captureMode,
|
|
3931
|
+
unsafeAllowRawCapture: Boolean(rawOpts.unsafeAllowRawCapture),
|
|
3932
|
+
captureStdinMode: rawOpts.captureStdinMode,
|
|
3933
|
+
captureStdoutBytes: rawOpts.captureStdoutBytes,
|
|
3934
|
+
captureStderrBytes: rawOpts.captureStderrBytes,
|
|
3935
|
+
captureArgvBytes: rawOpts.captureArgvBytes,
|
|
3936
|
+
envAllow: rawOpts.envAllow ?? [],
|
|
3937
|
+
envMode: rawOpts.envMode,
|
|
3938
|
+
unsafeAllowRawEnv: Boolean(rawOpts.unsafeAllowRawEnv),
|
|
3939
|
+
captureCwdMode: rawOpts.captureCwdMode,
|
|
3940
|
+
captureBinaryPath: rawOpts.captureBinaryPath,
|
|
3941
|
+
secretScan: rawOpts.secretScan === "on",
|
|
3942
|
+
unsafeDisableSecretScan: Boolean(rawOpts.unsafeDisableSecretScan),
|
|
3943
|
+
policyDigest: rawOpts.policyDigest,
|
|
3944
|
+
configDigest: rawOpts.configDigest,
|
|
3945
|
+
approvalRef: rawOpts.approvalRef,
|
|
3946
|
+
executionMode: rawOpts.executionMode,
|
|
3947
|
+
shellMode: Boolean(rawOpts.shellMode),
|
|
3948
|
+
output: rawOpts.output ?? "-",
|
|
3949
|
+
timeoutMs: rawOpts.timeoutMs,
|
|
3950
|
+
killGraceMs: rawOpts.killGraceMs,
|
|
3951
|
+
exitCodeMode: rawOpts.exitCodeMode
|
|
3952
|
+
};
|
|
3953
|
+
const result = await runObserveCommand(options, childArgv);
|
|
3954
|
+
process.exitCode = result.exitCode;
|
|
3955
|
+
});
|
|
3956
|
+
return cmd;
|
|
3957
|
+
}
|
|
3958
|
+
function observeCommand() {
|
|
3959
|
+
const observe = new Command("observe").description(
|
|
3960
|
+
"Observe local activity and emit unsigned observation records."
|
|
3961
|
+
);
|
|
3962
|
+
observe.addCommand(observeCommandSubcommand());
|
|
3963
|
+
return observe;
|
|
3964
|
+
}
|
|
3965
|
+
var IssuerKeyLoadError = class extends Error {
|
|
3966
|
+
constructor(message, cause) {
|
|
3967
|
+
super(message);
|
|
3968
|
+
this.cause = cause;
|
|
3969
|
+
this.name = "IssuerKeyLoadError";
|
|
3970
|
+
}
|
|
3971
|
+
code = "cli.issuer_key_load_failed";
|
|
3972
|
+
};
|
|
3973
|
+
var IssuerKeyInvalidError = class extends Error {
|
|
3974
|
+
constructor(message, cause) {
|
|
3975
|
+
super(message);
|
|
3976
|
+
this.cause = cause;
|
|
3977
|
+
this.name = "IssuerKeyInvalidError";
|
|
3978
|
+
}
|
|
3979
|
+
code = "cli.issuer_key_invalid";
|
|
3980
|
+
};
|
|
3981
|
+
async function loadIssuerKey(schemeUri, env = process.env) {
|
|
3982
|
+
let raw;
|
|
3983
|
+
if (schemeUri.startsWith("env:")) {
|
|
3984
|
+
const varName = schemeUri.slice(4);
|
|
3985
|
+
if (!varName) {
|
|
3986
|
+
throw new IssuerKeyLoadError(
|
|
3987
|
+
"env: scheme requires a variable name (e.g., env:PEAC_ISSUER_KEY)"
|
|
3988
|
+
);
|
|
3989
|
+
}
|
|
3990
|
+
const envValue = env[varName];
|
|
3991
|
+
if (!envValue) {
|
|
3992
|
+
throw new IssuerKeyLoadError(`environment variable ${varName} is not set or empty`);
|
|
3993
|
+
}
|
|
3994
|
+
raw = envValue;
|
|
3995
|
+
} else if (schemeUri.startsWith("file:")) {
|
|
3996
|
+
const filePath = schemeUri.slice(5);
|
|
3997
|
+
if (!filePath) {
|
|
3998
|
+
throw new IssuerKeyLoadError("file: scheme requires a path (e.g., file:/path/to/key.json)");
|
|
3999
|
+
}
|
|
4000
|
+
try {
|
|
4001
|
+
raw = await readFile(filePath, "utf-8");
|
|
4002
|
+
} catch (err) {
|
|
4003
|
+
throw new IssuerKeyLoadError(
|
|
4004
|
+
`failed to read key file: ${filePath} (${err?.code ?? (err instanceof Error ? err.message : String(err))})`,
|
|
4005
|
+
err
|
|
4006
|
+
);
|
|
4007
|
+
}
|
|
4008
|
+
} else {
|
|
4009
|
+
throw new IssuerKeyLoadError(
|
|
4010
|
+
`unsupported --issuer-key scheme: ${schemeUri} (expected env:VAR_NAME or file:/path)`
|
|
4011
|
+
);
|
|
4012
|
+
}
|
|
4013
|
+
let jwk;
|
|
4014
|
+
try {
|
|
4015
|
+
jwk = JSON.parse(raw);
|
|
4016
|
+
} catch (err) {
|
|
4017
|
+
throw new IssuerKeyInvalidError("issuer key is not valid JSON", err);
|
|
4018
|
+
}
|
|
4019
|
+
if (typeof jwk !== "object" || jwk === null || !("kty" in jwk) || !("crv" in jwk) || !("d" in jwk)) {
|
|
4020
|
+
throw new IssuerKeyInvalidError("JWK must contain kty, crv, and d fields");
|
|
4021
|
+
}
|
|
4022
|
+
const j = jwk;
|
|
4023
|
+
if (j.kty !== "OKP" || j.crv !== "Ed25519") {
|
|
4024
|
+
throw new IssuerKeyInvalidError(
|
|
4025
|
+
`JWK must be Ed25519 (kty: OKP, crv: Ed25519); got kty=${String(j.kty)} crv=${String(j.crv)}`
|
|
4026
|
+
);
|
|
4027
|
+
}
|
|
4028
|
+
if (typeof j.d !== "string" || typeof j.x !== "string") {
|
|
4029
|
+
throw new IssuerKeyInvalidError("JWK d and x fields must be base64url strings");
|
|
4030
|
+
}
|
|
4031
|
+
const ed25519Jwk = {
|
|
4032
|
+
kty: "OKP",
|
|
4033
|
+
crv: "Ed25519",
|
|
4034
|
+
x: j.x,
|
|
4035
|
+
d: j.d
|
|
4036
|
+
};
|
|
4037
|
+
const valid = await validateKeypair(ed25519Jwk);
|
|
4038
|
+
if (!valid) {
|
|
4039
|
+
throw new IssuerKeyInvalidError("JWK keypair validation failed: d does not derive to x");
|
|
4040
|
+
}
|
|
4041
|
+
const privateKey = base64urlDecode(ed25519Jwk.d);
|
|
4042
|
+
const publicKey = await derivePublicKey(privateKey);
|
|
4043
|
+
let kid;
|
|
4044
|
+
if (typeof j.kid === "string" && j.kid.length > 0) {
|
|
4045
|
+
kid = j.kid;
|
|
4046
|
+
} else {
|
|
4047
|
+
const pubB64 = base64urlEncode(publicKey);
|
|
4048
|
+
const hash = await sha256Hex$1(pubB64);
|
|
4049
|
+
kid = hash.slice(0, 16);
|
|
4050
|
+
}
|
|
4051
|
+
return { privateKey, publicKey, kid };
|
|
4052
|
+
}
|
|
4053
|
+
async function deriveKidFromPublicKey(publicKey) {
|
|
4054
|
+
const pubB64 = base64urlEncode(publicKey);
|
|
4055
|
+
const hash = await sha256Hex$1(pubB64);
|
|
4056
|
+
return hash.slice(0, 16);
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
// src/commands/record-command.ts
|
|
4060
|
+
var ARGV_MODES2 = ["hashed", "redacted", "raw"];
|
|
4061
|
+
var CWD_MODES2 = ["none", "hashed", "basename", "absolute"];
|
|
4062
|
+
var BINARY_PATH_MODES2 = ["none", "hashed", "absolute"];
|
|
4063
|
+
var STDIN_MODES2 = ["none", "length-only", "hashed"];
|
|
4064
|
+
var ENV_MODES2 = ["hashed", "raw"];
|
|
4065
|
+
var EXIT_CODE_MODES2 = ["child", "record"];
|
|
4066
|
+
var EXECUTION_MODES2 = [
|
|
4067
|
+
"deterministic_script",
|
|
4068
|
+
"templated_flow",
|
|
4069
|
+
"agent_loop",
|
|
4070
|
+
"human_step",
|
|
4071
|
+
"hybrid"
|
|
4072
|
+
];
|
|
4073
|
+
var RECORD_COMMAND_ERROR_CODES = {
|
|
4074
|
+
outputWriteFailed: "cli.output_write_failed",
|
|
4075
|
+
// Signing-specific (not part of the shared option-validation set).
|
|
4076
|
+
issuerKeyLoadFailed: "cli.issuer_key_load_failed",
|
|
4077
|
+
signingFailed: "cli.signing_failed"
|
|
4078
|
+
};
|
|
4079
|
+
var DEFAULT_OPTIONS2 = {
|
|
4080
|
+
captureMode: "hashed",
|
|
4081
|
+
unsafeAllowRawCapture: false,
|
|
4082
|
+
captureStdinMode: "none",
|
|
4083
|
+
captureStdoutBytes: CLI_LIMITS.defaultStdoutSampleBytes,
|
|
4084
|
+
captureStderrBytes: CLI_LIMITS.defaultStderrSampleBytes,
|
|
4085
|
+
captureArgvBytes: CLI_LIMITS.defaultArgvCaptureBytes,
|
|
4086
|
+
envAllow: [],
|
|
4087
|
+
envMode: "hashed",
|
|
4088
|
+
unsafeAllowRawEnv: false,
|
|
4089
|
+
captureCwdMode: "hashed",
|
|
4090
|
+
captureBinaryPath: "hashed",
|
|
4091
|
+
secretScan: true,
|
|
4092
|
+
unsafeDisableSecretScan: false,
|
|
4093
|
+
executionMode: "deterministic_script",
|
|
4094
|
+
shellMode: false,
|
|
4095
|
+
output: "-",
|
|
4096
|
+
timeoutMs: CLI_LIMITS.defaultTimeoutMs,
|
|
4097
|
+
killGraceMs: CLI_LIMITS.defaultKillGraceMs,
|
|
4098
|
+
exitCodeMode: "child",
|
|
4099
|
+
unsafeEphemeralKey: false
|
|
4100
|
+
};
|
|
4101
|
+
function validateRecordOptions(opts, childArgv) {
|
|
4102
|
+
const core = validateCoreCommandOptions(opts, childArgv);
|
|
4103
|
+
const programRequired = core.find((f) => f.code === COMMAND_OPTION_ERROR_CODES.programRequired);
|
|
4104
|
+
if (programRequired) return core;
|
|
4105
|
+
return [...core, ...validateSigningOptions(opts)];
|
|
4106
|
+
}
|
|
4107
|
+
async function resolveIssuerKey(opts, issuerKeyEnv) {
|
|
4108
|
+
if (opts.unsafeEphemeralKey) {
|
|
4109
|
+
const { privateKey, publicKey } = await generateKeypair();
|
|
4110
|
+
const kid = await deriveKidFromPublicKey(publicKey);
|
|
4111
|
+
return { ok: true, privateKey, kid };
|
|
4112
|
+
}
|
|
4113
|
+
try {
|
|
4114
|
+
const loaded = await loadIssuerKey(opts.issuerKey, issuerKeyEnv);
|
|
4115
|
+
return { ok: true, privateKey: loaded.privateKey, kid: loaded.kid };
|
|
4116
|
+
} catch (err) {
|
|
4117
|
+
if (err instanceof IssuerKeyInvalidError) {
|
|
4118
|
+
return { ok: false, code: err.code, message: err.message };
|
|
4119
|
+
}
|
|
4120
|
+
if (err instanceof IssuerKeyLoadError) {
|
|
4121
|
+
return { ok: false, code: err.code, message: err.message };
|
|
4122
|
+
}
|
|
4123
|
+
return {
|
|
4124
|
+
ok: false,
|
|
4125
|
+
code: RECORD_COMMAND_ERROR_CODES.issuerKeyLoadFailed,
|
|
4126
|
+
message: err instanceof Error ? err.message : String(err)
|
|
4127
|
+
};
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
async function runRecordCommand(options, childArgv, io = {}) {
|
|
4131
|
+
const opts = { ...DEFAULT_OPTIONS2, ...options };
|
|
4132
|
+
const writeStdout = io.writeStdout ?? ((c) => process.stdout.write(c));
|
|
4133
|
+
const writeStderr = io.writeStderr ?? ((c) => process.stderr.write(c));
|
|
4134
|
+
const childEnv = io.childEnv ?? process.env;
|
|
4135
|
+
const captureEnv = io.captureEnv ?? process.env;
|
|
4136
|
+
const issuerKeyEnv = io.issuerKeyEnv ?? process.env;
|
|
4137
|
+
const cwd = io.cwd ?? process.cwd();
|
|
4138
|
+
const peacCliVersion = io.peacCliVersion ?? "0.14.1";
|
|
4139
|
+
const failures = validateRecordOptions(opts, childArgv);
|
|
4140
|
+
if (failures.length > 0) {
|
|
4141
|
+
for (const f of failures) {
|
|
4142
|
+
writeStderr(`${f.code}: ${f.message}
|
|
4143
|
+
`);
|
|
4144
|
+
}
|
|
4145
|
+
return { exitCode: 2 };
|
|
4146
|
+
}
|
|
4147
|
+
const programToken = childArgv[0];
|
|
4148
|
+
const args = childArgv.slice(1);
|
|
4149
|
+
const outputErr = preflightOutputWritable2(opts.output);
|
|
4150
|
+
if (outputErr !== null) {
|
|
4151
|
+
writeStderr(`${RECORD_COMMAND_ERROR_CODES.outputWriteFailed}: ${outputErr}
|
|
4152
|
+
`);
|
|
4153
|
+
return { exitCode: 2 };
|
|
4154
|
+
}
|
|
4155
|
+
const keyResult = await resolveIssuerKey(opts, issuerKeyEnv);
|
|
4156
|
+
if (!keyResult.ok) {
|
|
4157
|
+
writeStderr(`${keyResult.code}: ${keyResult.message}
|
|
4158
|
+
`);
|
|
4159
|
+
return { exitCode: 2 };
|
|
4160
|
+
}
|
|
4161
|
+
const core = await runObservationCore(opts, programToken, args, {
|
|
4162
|
+
childEnv,
|
|
4163
|
+
captureEnv,
|
|
4164
|
+
cwd,
|
|
4165
|
+
peacCliVersion
|
|
4166
|
+
});
|
|
4167
|
+
if (!core.ok) {
|
|
4168
|
+
writeStderr(`${core.code}: ${core.message}
|
|
4169
|
+
`);
|
|
4170
|
+
return { exitCode: 2 };
|
|
4171
|
+
}
|
|
4172
|
+
let jws;
|
|
4173
|
+
try {
|
|
4174
|
+
const result = await issue({
|
|
4175
|
+
iss: opts.issuerId,
|
|
4176
|
+
kind: "evidence",
|
|
4177
|
+
type: CLI_COMMAND_EXECUTION_TYPE,
|
|
4178
|
+
privateKey: keyResult.privateKey,
|
|
4179
|
+
kid: keyResult.kid,
|
|
4180
|
+
extensions: {
|
|
4181
|
+
[CLI_EXECUTION_EXTENSION_KEY]: core.observation
|
|
4182
|
+
}
|
|
4183
|
+
});
|
|
4184
|
+
jws = result.jws;
|
|
4185
|
+
} catch (err) {
|
|
4186
|
+
const message = err instanceof IssueError ? err.message : err instanceof Error ? err.message : String(err);
|
|
4187
|
+
writeStderr(`${RECORD_COMMAND_ERROR_CODES.signingFailed}: ${message}
|
|
4188
|
+
`);
|
|
4189
|
+
return { exitCode: 2 };
|
|
4190
|
+
}
|
|
4191
|
+
if (opts.output === "-" || opts.output === "") {
|
|
4192
|
+
writeStdout(jws + "\n");
|
|
4193
|
+
} else {
|
|
4194
|
+
try {
|
|
4195
|
+
writeFileSync(opts.output, jws + "\n");
|
|
4196
|
+
} catch (err) {
|
|
4197
|
+
writeStderr(
|
|
4198
|
+
`${RECORD_COMMAND_ERROR_CODES.outputWriteFailed}: failed to write '${opts.output}': ${err instanceof Error ? err.message : String(err)}
|
|
4199
|
+
`
|
|
4200
|
+
);
|
|
4201
|
+
return { exitCode: 2 };
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
if (opts.exitCodeMode === "record") {
|
|
4205
|
+
return { exitCode: 0 };
|
|
4206
|
+
}
|
|
4207
|
+
return { exitCode: core.capture.exitCode };
|
|
4208
|
+
}
|
|
4209
|
+
function recordCommandSubcommand() {
|
|
4210
|
+
const cmd = new Command("command");
|
|
4211
|
+
cmd.description(
|
|
4212
|
+
"Wrap a command and emit a signed CLI execution record. record command is an OBSERVER, not a sandbox, permission system, shell orchestrator, or process supervisor. The command after `--` is spawned exactly as supplied (shell: false). Use --unsafe-ephemeral-key only for local development and tests; the public key is not published through normal issuer-key discovery."
|
|
4213
|
+
).addOption(
|
|
4214
|
+
new Option("--capture-mode <mode>", "argv capture mode").choices([...ARGV_MODES2]).default("hashed")
|
|
4215
|
+
).option("--unsafe-allow-raw-capture", "required alongside --capture-mode raw", false).addOption(
|
|
4216
|
+
new Option("--capture-stdin-mode <mode>", "stdin capture (and pass-through) mode").choices([...STDIN_MODES2]).default("none")
|
|
4217
|
+
).option(
|
|
4218
|
+
"--capture-stdout-bytes <n>",
|
|
4219
|
+
`stdout sample cap (raw mode only); max ${CLI_LIMITS.maxStdoutSampleBytes}`,
|
|
4220
|
+
parseIntegerFlag,
|
|
4221
|
+
CLI_LIMITS.defaultStdoutSampleBytes
|
|
4222
|
+
).option(
|
|
4223
|
+
"--capture-stderr-bytes <n>",
|
|
4224
|
+
`stderr sample cap (raw mode only); max ${CLI_LIMITS.maxStderrSampleBytes}`,
|
|
4225
|
+
parseIntegerFlag,
|
|
4226
|
+
CLI_LIMITS.defaultStderrSampleBytes
|
|
4227
|
+
).option(
|
|
4228
|
+
"--capture-argv-bytes <n>",
|
|
4229
|
+
`argv per-token byte cap (raw mode only); max ${CLI_LIMITS.maxArgvCaptureBytes}`,
|
|
4230
|
+
parseIntegerFlag,
|
|
4231
|
+
CLI_LIMITS.defaultArgvCaptureBytes
|
|
4232
|
+
).option(
|
|
4233
|
+
"--env-allow <KEYS>",
|
|
4234
|
+
"comma-separated allowlist of env vars to record (deny-by-default)",
|
|
4235
|
+
(v, prev) => prev.concat(
|
|
4236
|
+
v.split(",").map((s) => s.trim()).filter(Boolean)
|
|
4237
|
+
),
|
|
4238
|
+
[]
|
|
4239
|
+
).addOption(
|
|
4240
|
+
new Option("--env-mode <mode>", "env value capture mode").choices([...ENV_MODES2]).default("hashed")
|
|
4241
|
+
).option("--unsafe-allow-raw-env", "required alongside --env-mode raw", false).addOption(
|
|
4242
|
+
new Option("--capture-cwd-mode <mode>", "cwd capture mode").choices([...CWD_MODES2]).default("hashed")
|
|
4243
|
+
).addOption(
|
|
4244
|
+
new Option("--capture-binary-path <mode>", "binary path capture mode").choices([...BINARY_PATH_MODES2]).default("hashed")
|
|
4245
|
+
).addOption(
|
|
4246
|
+
new Option("--secret-scan <state>", "secret-scan over raw samples").choices(["on", "off"]).default("on")
|
|
4247
|
+
).option(
|
|
4248
|
+
"--unsafe-disable-secret-scan",
|
|
4249
|
+
"required to disable secret-scan when raw capture is enabled",
|
|
4250
|
+
false
|
|
4251
|
+
).option("--policy-digest <sha256>", "optional policy digest (sha256:<hex>)").option("--config-digest <sha256>", "optional config digest (sha256:<hex>)").option("--approval-ref <opaque>", "optional opaque approval reference").addOption(
|
|
4252
|
+
new Option("--execution-mode <mode>", "execution-mode tag for this record").choices([...EXECUTION_MODES2]).default("deterministic_script")
|
|
4253
|
+
).option(
|
|
4254
|
+
"--shell-mode",
|
|
4255
|
+
"acknowledge that the program is a shell binary; PEAC does NOT rewrite the command",
|
|
4256
|
+
false
|
|
4257
|
+
).option("--output <file>", "output path for the JWS (default: stdout)", "-").option(
|
|
4258
|
+
"--timeout-ms <n>",
|
|
4259
|
+
`wrapper timeout in ms; max ${CLI_LIMITS.maxTimeoutMs} (24h)`,
|
|
4260
|
+
parseIntegerFlag,
|
|
4261
|
+
CLI_LIMITS.defaultTimeoutMs
|
|
4262
|
+
).option(
|
|
4263
|
+
"--kill-grace-ms <n>",
|
|
4264
|
+
`SIGTERM-to-SIGKILL grace in ms; max ${CLI_LIMITS.maxKillGraceMs}`,
|
|
4265
|
+
parseIntegerFlag,
|
|
4266
|
+
CLI_LIMITS.defaultKillGraceMs
|
|
4267
|
+
).addOption(
|
|
4268
|
+
new Option("--exit-code-mode <mode>", "wrapper exit-code policy").choices([...EXIT_CODE_MODES2]).default("child")
|
|
4269
|
+
).option("--issuer-key <ref>", "issuer key reference: env:VAR_NAME or file:/path/to/jwk.json").option("--issuer-id <url>", "canonical issuer URL recorded as `iss`").option(
|
|
4270
|
+
"--unsafe-ephemeral-key",
|
|
4271
|
+
"Generates an ephemeral local signing key. The public key is not published through normal issuer-key discovery. Use only for local development and tests.",
|
|
4272
|
+
false
|
|
4273
|
+
).allowUnknownOption(false).allowExcessArguments(true);
|
|
4274
|
+
cmd.action(async (rawOpts, cmdInstance) => {
|
|
4275
|
+
const childArgv = cmdInstance.args;
|
|
4276
|
+
const options = {
|
|
4277
|
+
captureMode: rawOpts.captureMode,
|
|
4278
|
+
unsafeAllowRawCapture: Boolean(rawOpts.unsafeAllowRawCapture),
|
|
4279
|
+
captureStdinMode: rawOpts.captureStdinMode,
|
|
4280
|
+
captureStdoutBytes: rawOpts.captureStdoutBytes,
|
|
4281
|
+
captureStderrBytes: rawOpts.captureStderrBytes,
|
|
4282
|
+
captureArgvBytes: rawOpts.captureArgvBytes,
|
|
4283
|
+
envAllow: rawOpts.envAllow ?? [],
|
|
4284
|
+
envMode: rawOpts.envMode,
|
|
4285
|
+
unsafeAllowRawEnv: Boolean(rawOpts.unsafeAllowRawEnv),
|
|
4286
|
+
captureCwdMode: rawOpts.captureCwdMode,
|
|
4287
|
+
captureBinaryPath: rawOpts.captureBinaryPath,
|
|
4288
|
+
secretScan: rawOpts.secretScan === "on",
|
|
4289
|
+
unsafeDisableSecretScan: Boolean(rawOpts.unsafeDisableSecretScan),
|
|
4290
|
+
policyDigest: rawOpts.policyDigest,
|
|
4291
|
+
configDigest: rawOpts.configDigest,
|
|
4292
|
+
approvalRef: rawOpts.approvalRef,
|
|
4293
|
+
executionMode: rawOpts.executionMode,
|
|
4294
|
+
shellMode: Boolean(rawOpts.shellMode),
|
|
4295
|
+
output: rawOpts.output ?? "-",
|
|
4296
|
+
timeoutMs: rawOpts.timeoutMs,
|
|
4297
|
+
killGraceMs: rawOpts.killGraceMs,
|
|
4298
|
+
exitCodeMode: rawOpts.exitCodeMode,
|
|
4299
|
+
issuerKey: rawOpts.issuerKey,
|
|
4300
|
+
issuerId: rawOpts.issuerId,
|
|
4301
|
+
unsafeEphemeralKey: Boolean(rawOpts.unsafeEphemeralKey)
|
|
4302
|
+
};
|
|
4303
|
+
const result = await runRecordCommand(options, childArgv);
|
|
4304
|
+
process.exitCode = result.exitCode;
|
|
4305
|
+
});
|
|
4306
|
+
return cmd;
|
|
4307
|
+
}
|
|
4308
|
+
function recordCommand() {
|
|
4309
|
+
const record = new Command("record").description(
|
|
4310
|
+
"Sign and emit signed records over locally observed activity."
|
|
4311
|
+
);
|
|
4312
|
+
record.addCommand(recordCommandSubcommand());
|
|
4313
|
+
return record;
|
|
4314
|
+
}
|
|
4315
|
+
var EVENT_KINDS = [
|
|
4316
|
+
"lifecycle-approval-requested",
|
|
4317
|
+
"lifecycle-approval-granted",
|
|
4318
|
+
"lifecycle-approval-denied",
|
|
4319
|
+
"lifecycle-evaluation-started",
|
|
4320
|
+
"lifecycle-evaluation-completed",
|
|
4321
|
+
"lifecycle-experiment-assigned",
|
|
4322
|
+
"lifecycle-experiment-result",
|
|
4323
|
+
"lifecycle-workflow-transition",
|
|
4324
|
+
"lifecycle-mode-observed"
|
|
4325
|
+
];
|
|
4326
|
+
var TYPE_URI_BY_EVENT_KIND = (() => {
|
|
4327
|
+
const map = {};
|
|
4328
|
+
for (const k of EVENT_KINDS) {
|
|
4329
|
+
map[k] = `org.peacprotocol/${k}`;
|
|
4330
|
+
}
|
|
4331
|
+
for (const uri of LIFECYCLE_OBSERVATION_TYPE_URIS) {
|
|
4332
|
+
const k = uri.replace(/^org\.peacprotocol\//, "");
|
|
4333
|
+
if (!EVENT_KINDS.includes(k)) {
|
|
4334
|
+
throw new Error(`emit-lifecycle: type URI ${uri} has no matching event_kind discriminator`);
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
return map;
|
|
4338
|
+
})();
|
|
4339
|
+
var OBSERVED_MODES = [
|
|
4340
|
+
"deterministic_script",
|
|
4341
|
+
"templated_flow",
|
|
4342
|
+
"agent_loop",
|
|
4343
|
+
"human_step",
|
|
4344
|
+
"hybrid"
|
|
4345
|
+
];
|
|
4346
|
+
var EMIT_LIFECYCLE_ERROR_CODES = {
|
|
4347
|
+
missingRequiredField: "lifecycle.missing_required_field",
|
|
4348
|
+
eventKindUnknown: "lifecycle.event_kind_unknown",
|
|
4349
|
+
invalidObservedAt: "lifecycle.invalid_observed_at",
|
|
4350
|
+
invalidObservedMode: "lifecycle.invalid_observed_mode",
|
|
4351
|
+
invalidState: "lifecycle.invalid_state",
|
|
4352
|
+
issuerKeyRequired: "cli.issuer_key_required",
|
|
4353
|
+
issuerIdRequired: "cli.issuer_id_required",
|
|
4354
|
+
issuerIdInvalid: "cli.issuer_id_invalid",
|
|
4355
|
+
unsafeEphemeralKeyMutex: "cli.unsafe_ephemeral_key_mutex",
|
|
4356
|
+
issuerKeyLoadFailed: "cli.issuer_key_load_failed",
|
|
4357
|
+
signingFailed: "cli.signing_failed",
|
|
4358
|
+
outputWriteFailed: "cli.output_write_failed"
|
|
4359
|
+
};
|
|
4360
|
+
var RFC_3339_WITH_OFFSET = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
4361
|
+
function validateEmitLifecycleOptions(opts) {
|
|
4362
|
+
const failures = [];
|
|
4363
|
+
if (!opts.eventKind) {
|
|
4364
|
+
failures.push({
|
|
4365
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4366
|
+
message: "--event-kind is required"
|
|
4367
|
+
});
|
|
4368
|
+
} else if (!EVENT_KINDS.includes(opts.eventKind)) {
|
|
4369
|
+
failures.push({
|
|
4370
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.eventKindUnknown,
|
|
4371
|
+
message: `--event-kind must be one of: ${EVENT_KINDS.join(", ")}`
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4374
|
+
if (!opts.subjectRef) {
|
|
4375
|
+
failures.push({
|
|
4376
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4377
|
+
message: "--subject-ref is required"
|
|
4378
|
+
});
|
|
4379
|
+
}
|
|
4380
|
+
if (!opts.observedAt) {
|
|
4381
|
+
failures.push({
|
|
4382
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4383
|
+
message: "--observed-at is required (no default-now; provide the wall-clock time at which the EXTERNAL system observed the event)"
|
|
4384
|
+
});
|
|
4385
|
+
} else if (!RFC_3339_WITH_OFFSET.test(opts.observedAt)) {
|
|
4386
|
+
failures.push({
|
|
4387
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.invalidObservedAt,
|
|
4388
|
+
message: "--observed-at must be RFC 3339 with timezone offset (e.g., 2026-05-12T10:00:00Z)"
|
|
4389
|
+
});
|
|
4390
|
+
}
|
|
4391
|
+
const ek = opts.eventKind;
|
|
4392
|
+
if (ek && EVENT_KINDS.includes(ek)) {
|
|
4393
|
+
if (ek === "lifecycle-approval-requested" || ek === "lifecycle-approval-granted" || ek === "lifecycle-approval-denied") {
|
|
4394
|
+
if (!opts.approvalRef) {
|
|
4395
|
+
failures.push({
|
|
4396
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4397
|
+
message: `--approval-ref is required for event-kind ${ek}`
|
|
4398
|
+
});
|
|
4399
|
+
}
|
|
4400
|
+
if (!opts.approverRef) {
|
|
4401
|
+
failures.push({
|
|
4402
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4403
|
+
message: `--approver-ref is required for event-kind ${ek}`
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4406
|
+
} else if (ek === "lifecycle-evaluation-completed") {
|
|
4407
|
+
if (!opts.resultRef) {
|
|
4408
|
+
failures.push({
|
|
4409
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4410
|
+
message: "--result-ref is required for event-kind lifecycle-evaluation-completed"
|
|
4411
|
+
});
|
|
4412
|
+
}
|
|
4413
|
+
} else if (ek === "lifecycle-experiment-assigned") {
|
|
4414
|
+
if (!opts.experimentRef) {
|
|
4415
|
+
failures.push({
|
|
4416
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4417
|
+
message: "--experiment-ref is required for event-kind lifecycle-experiment-assigned"
|
|
4418
|
+
});
|
|
4419
|
+
}
|
|
4420
|
+
} else if (ek === "lifecycle-experiment-result") {
|
|
4421
|
+
if (!opts.experimentRef) {
|
|
4422
|
+
failures.push({
|
|
4423
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4424
|
+
message: "--experiment-ref is required for event-kind lifecycle-experiment-result"
|
|
4425
|
+
});
|
|
4426
|
+
}
|
|
4427
|
+
if (!opts.resultRef) {
|
|
4428
|
+
failures.push({
|
|
4429
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4430
|
+
message: "--result-ref is required for event-kind lifecycle-experiment-result"
|
|
4431
|
+
});
|
|
4432
|
+
}
|
|
4433
|
+
} else if (ek === "lifecycle-workflow-transition") {
|
|
4434
|
+
if (!opts.fromState) {
|
|
4435
|
+
failures.push({
|
|
4436
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4437
|
+
message: "--from-state is required for event-kind lifecycle-workflow-transition"
|
|
4438
|
+
});
|
|
4439
|
+
}
|
|
4440
|
+
if (!opts.toState) {
|
|
4441
|
+
failures.push({
|
|
4442
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4443
|
+
message: "--to-state is required for event-kind lifecycle-workflow-transition"
|
|
4444
|
+
});
|
|
4445
|
+
}
|
|
4446
|
+
} else if (ek === "lifecycle-mode-observed") {
|
|
4447
|
+
if (!opts.observedMode) {
|
|
4448
|
+
failures.push({
|
|
4449
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.missingRequiredField,
|
|
4450
|
+
message: "--observed-mode is required for event-kind lifecycle-mode-observed"
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
if (opts.observedMode && !OBSERVED_MODES.includes(opts.observedMode)) {
|
|
4456
|
+
failures.push({
|
|
4457
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.invalidObservedMode,
|
|
4458
|
+
message: `--observed-mode must be one of: ${OBSERVED_MODES.join(", ")}`
|
|
4459
|
+
});
|
|
4460
|
+
}
|
|
4461
|
+
for (const [flag, value] of [
|
|
4462
|
+
["--from-state", opts.fromState],
|
|
4463
|
+
["--to-state", opts.toState]
|
|
4464
|
+
]) {
|
|
4465
|
+
if (value !== void 0) {
|
|
4466
|
+
if (value.length === 0) {
|
|
4467
|
+
failures.push({
|
|
4468
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.invalidState,
|
|
4469
|
+
message: `${flag} must not be empty`
|
|
4470
|
+
});
|
|
4471
|
+
} else if (/^\s|\s$/.test(value)) {
|
|
4472
|
+
failures.push({
|
|
4473
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.invalidState,
|
|
4474
|
+
message: `${flag} must not have leading or trailing whitespace`
|
|
4475
|
+
});
|
|
4476
|
+
} else if (new TextEncoder().encode(value).byteLength > 256) {
|
|
4477
|
+
failures.push({
|
|
4478
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.invalidState,
|
|
4479
|
+
message: `${flag} must be <= 256 UTF-8 bytes`
|
|
4480
|
+
});
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
const hasIssuerKey = !!opts.issuerKey;
|
|
4485
|
+
if (!hasIssuerKey && !opts.unsafeEphemeralKey) {
|
|
4486
|
+
failures.push({
|
|
4487
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.issuerKeyRequired,
|
|
4488
|
+
message: "one of --issuer-key or --unsafe-ephemeral-key is required"
|
|
4489
|
+
});
|
|
4490
|
+
} else if (hasIssuerKey && opts.unsafeEphemeralKey) {
|
|
4491
|
+
failures.push({
|
|
4492
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.unsafeEphemeralKeyMutex,
|
|
4493
|
+
message: "--issuer-key and --unsafe-ephemeral-key are mutually exclusive"
|
|
4494
|
+
});
|
|
4495
|
+
}
|
|
4496
|
+
if (!opts.issuerId) {
|
|
4497
|
+
failures.push({
|
|
4498
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.issuerIdRequired,
|
|
4499
|
+
message: "--issuer-id is required"
|
|
4500
|
+
});
|
|
4501
|
+
} else if (!isCanonicalIss(opts.issuerId)) {
|
|
4502
|
+
failures.push({
|
|
4503
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.issuerIdInvalid,
|
|
4504
|
+
message: "--issuer-id must be a canonical https://<origin> URL or did:<method>:<id> identifier"
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
4507
|
+
return failures;
|
|
4508
|
+
}
|
|
4509
|
+
async function resolveIssuerKey2(opts, issuerKeyEnv) {
|
|
4510
|
+
if (opts.unsafeEphemeralKey) {
|
|
4511
|
+
const { privateKey, publicKey } = await generateKeypair();
|
|
4512
|
+
const kid = await deriveKidFromPublicKey(publicKey);
|
|
4513
|
+
return { ok: true, privateKey, kid };
|
|
4514
|
+
}
|
|
4515
|
+
try {
|
|
4516
|
+
const loaded = await loadIssuerKey(opts.issuerKey, issuerKeyEnv);
|
|
4517
|
+
return { ok: true, privateKey: loaded.privateKey, kid: loaded.kid };
|
|
4518
|
+
} catch (err) {
|
|
4519
|
+
if (err instanceof IssuerKeyInvalidError) {
|
|
4520
|
+
return { ok: false, code: err.code, message: err.message };
|
|
4521
|
+
}
|
|
4522
|
+
if (err instanceof IssuerKeyLoadError) {
|
|
4523
|
+
return { ok: false, code: err.code, message: err.message };
|
|
4524
|
+
}
|
|
4525
|
+
return {
|
|
4526
|
+
ok: false,
|
|
4527
|
+
code: EMIT_LIFECYCLE_ERROR_CODES.issuerKeyLoadFailed,
|
|
4528
|
+
message: err instanceof Error ? err.message : String(err)
|
|
4529
|
+
};
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
function buildLifecycleObservation(opts) {
|
|
4533
|
+
const obs = {
|
|
4534
|
+
event_kind: opts.eventKind,
|
|
4535
|
+
subject_ref: opts.subjectRef,
|
|
4536
|
+
observed_at: opts.observedAt
|
|
4537
|
+
};
|
|
4538
|
+
if (opts.parentRef) obs.parent_ref = opts.parentRef;
|
|
4539
|
+
if (opts.upstreamArtifactRef) obs.upstream_artifact_ref = opts.upstreamArtifactRef;
|
|
4540
|
+
if (opts.upstreamArtifactDigest) obs.upstream_artifact_digest = opts.upstreamArtifactDigest;
|
|
4541
|
+
if (opts.policyRef) obs.policy_ref = opts.policyRef;
|
|
4542
|
+
if (opts.policyDigest) obs.policy_digest = opts.policyDigest;
|
|
4543
|
+
if (opts.rubricRef) obs.rubric_ref = opts.rubricRef;
|
|
4544
|
+
if (opts.scoreRef) obs.score_ref = opts.scoreRef;
|
|
4545
|
+
if (opts.resultDigest) obs.result_digest = opts.resultDigest;
|
|
4546
|
+
const ek = opts.eventKind;
|
|
4547
|
+
if (ek === "lifecycle-approval-requested" || ek === "lifecycle-approval-granted" || ek === "lifecycle-approval-denied") {
|
|
4548
|
+
obs.approval_ref = opts.approvalRef;
|
|
4549
|
+
obs.approver_ref = opts.approverRef;
|
|
4550
|
+
} else if (ek === "lifecycle-evaluation-completed") {
|
|
4551
|
+
obs.result_ref = opts.resultRef;
|
|
4552
|
+
} else if (ek === "lifecycle-experiment-assigned") {
|
|
4553
|
+
obs.experiment_ref = opts.experimentRef;
|
|
4554
|
+
if (opts.cohortRef) obs.cohort_ref = opts.cohortRef;
|
|
4555
|
+
if (opts.variantRef) obs.variant_ref = opts.variantRef;
|
|
4556
|
+
} else if (ek === "lifecycle-experiment-result") {
|
|
4557
|
+
obs.experiment_ref = opts.experimentRef;
|
|
4558
|
+
obs.result_ref = opts.resultRef;
|
|
4559
|
+
if (opts.cohortRef) obs.cohort_ref = opts.cohortRef;
|
|
4560
|
+
if (opts.variantRef) obs.variant_ref = opts.variantRef;
|
|
4561
|
+
} else if (ek === "lifecycle-workflow-transition") {
|
|
4562
|
+
obs.from_state = opts.fromState;
|
|
4563
|
+
obs.to_state = opts.toState;
|
|
4564
|
+
} else if (ek === "lifecycle-mode-observed") {
|
|
4565
|
+
obs.observed_mode = opts.observedMode;
|
|
4566
|
+
}
|
|
4567
|
+
if (ek !== "lifecycle-mode-observed" && opts.observedMode) {
|
|
4568
|
+
obs.observed_mode = opts.observedMode;
|
|
4569
|
+
}
|
|
4570
|
+
return obs;
|
|
4571
|
+
}
|
|
4572
|
+
async function runEmitLifecycle(options, io = {}) {
|
|
4573
|
+
const opts = {
|
|
4574
|
+
unsafeEphemeralKey: false,
|
|
4575
|
+
output: "-",
|
|
4576
|
+
...options
|
|
4577
|
+
};
|
|
4578
|
+
const writeStdout = io.writeStdout ?? ((c) => process.stdout.write(c));
|
|
4579
|
+
const writeStderr = io.writeStderr ?? ((c) => process.stderr.write(c));
|
|
4580
|
+
const issuerKeyEnv = io.issuerKeyEnv ?? process.env;
|
|
4581
|
+
const failures = validateEmitLifecycleOptions(opts);
|
|
4582
|
+
if (failures.length > 0) {
|
|
4583
|
+
for (const f of failures) {
|
|
4584
|
+
writeStderr(`${f.code}: ${f.message}
|
|
4585
|
+
`);
|
|
4586
|
+
}
|
|
4587
|
+
return { exitCode: 2 };
|
|
4588
|
+
}
|
|
4589
|
+
const outputErr = preflightOutputWritable(opts.output);
|
|
4590
|
+
if (outputErr !== null) {
|
|
4591
|
+
writeStderr(`${EMIT_LIFECYCLE_ERROR_CODES.outputWriteFailed}: ${outputErr}
|
|
4592
|
+
`);
|
|
4593
|
+
return { exitCode: 2 };
|
|
4594
|
+
}
|
|
4595
|
+
const keyResult = await resolveIssuerKey2(opts, issuerKeyEnv);
|
|
4596
|
+
if (!keyResult.ok) {
|
|
4597
|
+
writeStderr(`${keyResult.code}: ${keyResult.message}
|
|
4598
|
+
`);
|
|
4599
|
+
return { exitCode: 2 };
|
|
4600
|
+
}
|
|
4601
|
+
const observation = buildLifecycleObservation(opts);
|
|
4602
|
+
const validation = validateLifecycleObservation(observation);
|
|
4603
|
+
if (!validation.ok) {
|
|
4604
|
+
for (const e of validation.errors) {
|
|
4605
|
+
writeStderr(`${e.code}: ${e.message}
|
|
4606
|
+
`);
|
|
4607
|
+
}
|
|
4608
|
+
return { exitCode: 2 };
|
|
4609
|
+
}
|
|
4610
|
+
let jws;
|
|
4611
|
+
try {
|
|
4612
|
+
const recordType = TYPE_URI_BY_EVENT_KIND[opts.eventKind];
|
|
4613
|
+
const result = await issue({
|
|
4614
|
+
iss: opts.issuerId,
|
|
4615
|
+
kind: "evidence",
|
|
4616
|
+
type: recordType,
|
|
4617
|
+
privateKey: keyResult.privateKey,
|
|
4618
|
+
kid: keyResult.kid,
|
|
4619
|
+
extensions: {
|
|
4620
|
+
[LIFECYCLE_OBSERVATION_EXTENSION_KEY]: validation.value
|
|
4621
|
+
}
|
|
4622
|
+
});
|
|
4623
|
+
jws = result.jws;
|
|
4624
|
+
} catch (err) {
|
|
4625
|
+
const message = err instanceof IssueError ? err.message : err instanceof Error ? err.message : String(err);
|
|
4626
|
+
writeStderr(`${EMIT_LIFECYCLE_ERROR_CODES.signingFailed}: ${message}
|
|
4627
|
+
`);
|
|
4628
|
+
return { exitCode: 2 };
|
|
4629
|
+
}
|
|
4630
|
+
if (opts.output === "-" || opts.output === "") {
|
|
4631
|
+
writeStdout(jws + "\n");
|
|
4632
|
+
} else {
|
|
4633
|
+
try {
|
|
4634
|
+
writeFileSync(opts.output, jws + "\n");
|
|
4635
|
+
} catch (err) {
|
|
4636
|
+
writeStderr(
|
|
4637
|
+
`${EMIT_LIFECYCLE_ERROR_CODES.outputWriteFailed}: failed to write '${opts.output}': ${err instanceof Error ? err.message : String(err)}
|
|
4638
|
+
`
|
|
4639
|
+
);
|
|
4640
|
+
return { exitCode: 2 };
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
return { exitCode: 0 };
|
|
4644
|
+
}
|
|
4645
|
+
function emitLifecycleSubcommand() {
|
|
4646
|
+
const cmd = new Command("lifecycle");
|
|
4647
|
+
cmd.description(
|
|
4648
|
+
"Issue a signed lifecycle observation record from caller-supplied flags. The caller is the observer (the lifecycle event was emitted by an external orchestrator, workflow engine, evaluation system, approval system, or agent runtime); the CLI is the issuance path; the caller-provided issuer key is the signer-of-record. PEAC does not approve, evaluate, score, transition, orchestrate, schedule, or vouch for the truth of the event. The full contract is specified in docs/specs/LIFECYCLE-OBSERVATION-PROFILE.md."
|
|
4649
|
+
).option(
|
|
4650
|
+
"--event-kind <kind>",
|
|
4651
|
+
`one of the 9 lifecycle event-kind discriminator values: ${EVENT_KINDS.join(", ")}`
|
|
4652
|
+
).option("--subject-ref <opaque>", "opaque reference to the subject of the observation").option("--parent-ref <opaque>", "optional opaque reference to a parent observation").option("--upstream-artifact-ref <opaque>", "optional opaque reference to an upstream artifact").option(
|
|
4653
|
+
"--upstream-artifact-digest <sha256>",
|
|
4654
|
+
"optional canonical sha256 digest of the upstream artifact"
|
|
4655
|
+
).option("--policy-ref <opaque>", "optional opaque reference to an upstream policy document").option("--policy-digest <sha256>", "optional canonical sha256 digest of the policy document").option("--rubric-ref <opaque>", "optional opaque reference to an evaluation rubric").option(
|
|
4656
|
+
"--approval-ref <opaque>",
|
|
4657
|
+
"opaque reference to the approval (REQUIRED for approval-* event kinds)"
|
|
4658
|
+
).option(
|
|
4659
|
+
"--approver-ref <opaque>",
|
|
4660
|
+
"opaque pseudonymous reference to the approver (REQUIRED for approval-* event kinds; @-containing values reject as PII)"
|
|
4661
|
+
).option(
|
|
4662
|
+
"--experiment-ref <opaque>",
|
|
4663
|
+
"opaque reference to the experiment (REQUIRED for experiment-* event kinds)"
|
|
4664
|
+
).option("--cohort-ref <opaque>", "optional opaque reference to the experiment cohort").option("--variant-ref <opaque>", "optional opaque reference to the experiment variant").option("--observed-mode <mode>", `observed runtime-mode tag: ${OBSERVED_MODES.join(", ")}`).option(
|
|
4665
|
+
"--result-ref <opaque>",
|
|
4666
|
+
"opaque reference to a stored result artifact (REQUIRED for evaluation-completed and experiment-result)"
|
|
4667
|
+
).option("--result-digest <sha256>", "optional canonical sha256 digest of the result artifact").option(
|
|
4668
|
+
"--score-ref <opaque>",
|
|
4669
|
+
"optional opaque reference to a stored score artifact (score values are NEVER inlined)"
|
|
4670
|
+
).option(
|
|
4671
|
+
"--from-state <name>",
|
|
4672
|
+
"free-form source state name (REQUIRED for workflow-transition; max 128 chars)"
|
|
4673
|
+
).option(
|
|
4674
|
+
"--to-state <name>",
|
|
4675
|
+
"free-form destination state name (REQUIRED for workflow-transition; max 128 chars)"
|
|
4676
|
+
).option(
|
|
4677
|
+
"--observed-at <rfc3339>",
|
|
4678
|
+
"RFC 3339 timestamp at which the EXTERNAL system observed the event (REQUIRED; no default-now)"
|
|
4679
|
+
).option("--issuer-key <ref>", "issuer key reference: env:VAR_NAME or file:/path/to/jwk.json").option("--issuer-id <url>", "canonical issuer URL recorded as `iss`").option(
|
|
4680
|
+
"--unsafe-ephemeral-key",
|
|
4681
|
+
"Generate an ephemeral local signing key. The public key is not published through normal issuer-key discovery. Use only for local development and tests.",
|
|
4682
|
+
false
|
|
4683
|
+
).option("--output <file>", "output path for the JWS (default: stdout)", "-");
|
|
4684
|
+
cmd.action(async (rawOpts) => {
|
|
4685
|
+
const options = {
|
|
4686
|
+
eventKind: rawOpts.eventKind,
|
|
4687
|
+
subjectRef: rawOpts.subjectRef,
|
|
4688
|
+
parentRef: rawOpts.parentRef,
|
|
4689
|
+
upstreamArtifactRef: rawOpts.upstreamArtifactRef,
|
|
4690
|
+
upstreamArtifactDigest: rawOpts.upstreamArtifactDigest,
|
|
4691
|
+
policyRef: rawOpts.policyRef,
|
|
4692
|
+
policyDigest: rawOpts.policyDigest,
|
|
4693
|
+
rubricRef: rawOpts.rubricRef,
|
|
4694
|
+
approvalRef: rawOpts.approvalRef,
|
|
4695
|
+
approverRef: rawOpts.approverRef,
|
|
4696
|
+
experimentRef: rawOpts.experimentRef,
|
|
4697
|
+
cohortRef: rawOpts.cohortRef,
|
|
4698
|
+
variantRef: rawOpts.variantRef,
|
|
4699
|
+
observedMode: rawOpts.observedMode,
|
|
4700
|
+
resultRef: rawOpts.resultRef,
|
|
4701
|
+
resultDigest: rawOpts.resultDigest,
|
|
4702
|
+
scoreRef: rawOpts.scoreRef,
|
|
4703
|
+
fromState: rawOpts.fromState,
|
|
4704
|
+
toState: rawOpts.toState,
|
|
4705
|
+
observedAt: rawOpts.observedAt,
|
|
4706
|
+
issuerKey: rawOpts.issuerKey,
|
|
4707
|
+
issuerId: rawOpts.issuerId,
|
|
4708
|
+
unsafeEphemeralKey: Boolean(rawOpts.unsafeEphemeralKey),
|
|
4709
|
+
output: rawOpts.output ?? "-"
|
|
4710
|
+
};
|
|
4711
|
+
const result = await runEmitLifecycle(options);
|
|
4712
|
+
process.exitCode = result.exitCode;
|
|
4713
|
+
});
|
|
4714
|
+
return cmd;
|
|
4715
|
+
}
|
|
4716
|
+
function emitCommand() {
|
|
4717
|
+
const emit = new Command("emit").description(
|
|
4718
|
+
"Issue signed records over events emitted by external systems (caller-supplied event emission/export surface)."
|
|
4719
|
+
);
|
|
4720
|
+
emit.addCommand(emitLifecycleSubcommand());
|
|
4721
|
+
return emit;
|
|
4722
|
+
}
|
|
4723
|
+
|
|
2807
4724
|
// src/index.ts
|
|
2808
4725
|
var program = new Command();
|
|
2809
4726
|
program.name("peac").description("PEAC protocol command-line tools").version(getVersion()).exitOverride();
|
|
@@ -2939,6 +4856,9 @@ program.addCommand(policy);
|
|
|
2939
4856
|
program.addCommand(conformance);
|
|
2940
4857
|
program.addCommand(samples);
|
|
2941
4858
|
program.addCommand(reconcileCommand());
|
|
4859
|
+
program.addCommand(observeCommand());
|
|
4860
|
+
program.addCommand(recordCommand());
|
|
4861
|
+
program.addCommand(emitCommand());
|
|
2942
4862
|
doctor(program);
|
|
2943
4863
|
program.command("discover").description("Discover and parse a remote /.well-known/peac.txt policy document").argument("<url>", "Origin URL (http/https) whose /.well-known/peac.txt to fetch").option("-j, --json", "output in JSON format").action(async (url, opts) => {
|
|
2944
4864
|
const cmd = new DiscoverCommand();
|