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