@lousy-agents/agent-shell 5.8.8 → 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -15
- package/dist/index.js +2568 -1875
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -425,6 +425,8 @@ const external_node_child_process_namespaceObject = __rspack_createRequire_requi
|
|
|
425
425
|
const external_node_crypto_namespaceObject = __rspack_createRequire_require("node:crypto");
|
|
426
426
|
;// CONCATENATED MODULE: external "node:fs/promises"
|
|
427
427
|
const promises_namespaceObject = __rspack_createRequire_require("node:fs/promises");
|
|
428
|
+
;// CONCATENATED MODULE: external "node:readline"
|
|
429
|
+
const external_node_readline_namespaceObject = __rspack_createRequire_require("node:readline");
|
|
428
430
|
;// CONCATENATED MODULE: external "node:path"
|
|
429
431
|
const external_node_path_namespaceObject = __rspack_createRequire_require("node:path");
|
|
430
432
|
;// CONCATENATED MODULE: ./src/git-utils.ts
|
|
@@ -490,73 +492,6 @@ function defaultExecutor(env) {
|
|
|
490
492
|
}
|
|
491
493
|
/** Process-scoped singleton for production use. */ const git_utils_getRepositoryRoot = createGetRepositoryRoot();
|
|
492
494
|
|
|
493
|
-
;// CONCATENATED MODULE: external "node:fs"
|
|
494
|
-
const external_node_fs_namespaceObject = __rspack_createRequire_require("node:fs");
|
|
495
|
-
;// CONCATENATED MODULE: external "node:readline"
|
|
496
|
-
const external_node_readline_namespaceObject = __rspack_createRequire_require("node:readline");
|
|
497
|
-
;// CONCATENATED MODULE: ./src/log/format.ts
|
|
498
|
-
function formatDuration(ms) {
|
|
499
|
-
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
500
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
501
|
-
return `${(ms / 60000).toFixed(1)}m`;
|
|
502
|
-
}
|
|
503
|
-
function formatTimestamp(iso) {
|
|
504
|
-
const d = new Date(iso);
|
|
505
|
-
return d.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "");
|
|
506
|
-
}
|
|
507
|
-
function truncateCommand(command, maxLen) {
|
|
508
|
-
if (command.length <= maxLen) return command;
|
|
509
|
-
return `${command.slice(0, maxLen - 3)}...`;
|
|
510
|
-
}
|
|
511
|
-
function padRight(str, width) {
|
|
512
|
-
if (str.length >= width) return str;
|
|
513
|
-
return str + " ".repeat(width - str.length);
|
|
514
|
-
}
|
|
515
|
-
const COL_TIMESTAMP = 21;
|
|
516
|
-
const COL_SCRIPT = 9;
|
|
517
|
-
const COL_ACTOR = 13;
|
|
518
|
-
const COL_EXIT = 6;
|
|
519
|
-
const COL_DURATION = 10;
|
|
520
|
-
const MAX_COMMAND_LEN = 50;
|
|
521
|
-
function formatEventsTable(events) {
|
|
522
|
-
const header = padRight("TIMESTAMP", COL_TIMESTAMP) + padRight("SCRIPT", COL_SCRIPT) + padRight("ACTOR", COL_ACTOR) + padRight("EXIT", COL_EXIT) + padRight("DURATION", COL_DURATION) + "COMMAND";
|
|
523
|
-
const rows = events.map((event)=>{
|
|
524
|
-
const timestamp = formatTimestamp(event.timestamp);
|
|
525
|
-
const script = event.event === "script_end" ? event.script ?? "-" : "-";
|
|
526
|
-
const actor = event.actor;
|
|
527
|
-
const exitCode = event.event === "script_end" ? String(event.exit_code) : "-";
|
|
528
|
-
const duration = event.event === "script_end" ? formatDuration(event.duration_ms) : "-";
|
|
529
|
-
const command = truncateCommand(event.command, MAX_COMMAND_LEN);
|
|
530
|
-
return padRight(timestamp, COL_TIMESTAMP) + padRight(script, COL_SCRIPT) + padRight(actor, COL_ACTOR) + padRight(exitCode, COL_EXIT) + padRight(duration, COL_DURATION) + command;
|
|
531
|
-
});
|
|
532
|
-
return [
|
|
533
|
-
header,
|
|
534
|
-
...rows
|
|
535
|
-
].join("\n");
|
|
536
|
-
}
|
|
537
|
-
function formatSessionsTable(sessions) {
|
|
538
|
-
const colSession = 11;
|
|
539
|
-
const colFirstEvent = 21;
|
|
540
|
-
const colLastEvent = 21;
|
|
541
|
-
const colEvents = 9;
|
|
542
|
-
const header = padRight("SESSION", colSession) + padRight("FIRST EVENT", colFirstEvent) + padRight("LAST EVENT", colLastEvent) + padRight("EVENTS", colEvents) + "ACTORS";
|
|
543
|
-
const rows = sessions.map((s)=>{
|
|
544
|
-
const sessionTrunc = s.sessionId.slice(0, 8);
|
|
545
|
-
const firstEvent = formatTimestamp(s.firstEvent);
|
|
546
|
-
const lastEvent = formatTimestamp(s.lastEvent);
|
|
547
|
-
const eventCount = String(s.eventCount);
|
|
548
|
-
const actors = s.actors.join(", ");
|
|
549
|
-
return padRight(sessionTrunc, colSession) + padRight(firstEvent, colFirstEvent) + padRight(lastEvent, colLastEvent) + padRight(eventCount, colEvents) + actors;
|
|
550
|
-
});
|
|
551
|
-
return [
|
|
552
|
-
header,
|
|
553
|
-
...rows
|
|
554
|
-
].join("\n");
|
|
555
|
-
}
|
|
556
|
-
function formatEventsJson(events) {
|
|
557
|
-
return JSON.stringify(events, null, 2);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
495
|
;// CONCATENATED MODULE: ./src/path-utils.ts
|
|
561
496
|
|
|
562
497
|
function isWithinProjectRoot(resolvedPath, projectRoot) {
|
|
@@ -14310,2031 +14245,2710 @@ core_config(en());
|
|
|
14310
14245
|
|
|
14311
14246
|
/* export default */ const v4 = ((/* unused pure expression or super */ null && (z4)));
|
|
14312
14247
|
|
|
14313
|
-
;// CONCATENATED MODULE: ./src/
|
|
14314
|
-
|
|
14248
|
+
;// CONCATENATED MODULE: ./src/sanitize.ts
|
|
14249
|
+
/**
|
|
14250
|
+
* Escapes ASCII and C1 control characters in error messages before writing
|
|
14251
|
+
* to stderr. Replaces each control character with its `\xNN` hex
|
|
14252
|
+
* representation to prevent log/terminal injection when errors embed
|
|
14253
|
+
* untrusted data.
|
|
14254
|
+
*/ function sanitizeForStderr(err) {
|
|
14255
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14256
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentionally matching control characters for sanitization
|
|
14257
|
+
return msg.replace(/[\u0000-\u001f\u007f-\u009f]/g, (ch)=>{
|
|
14258
|
+
return `\\x${ch.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
|
14259
|
+
});
|
|
14260
|
+
}
|
|
14261
|
+
/**
|
|
14262
|
+
* Escapes ASCII and C1 control characters (except newline) in output text.
|
|
14263
|
+
* Like sanitizeForStderr but preserves newlines for JSON formatting.
|
|
14264
|
+
*/ function sanitizeOutput(text) {
|
|
14265
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentionally matching control characters for sanitization
|
|
14266
|
+
return text.replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g, (ch)=>{
|
|
14267
|
+
return `\\x${ch.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
|
14268
|
+
});
|
|
14269
|
+
}
|
|
14270
|
+
/**
|
|
14271
|
+
* Sanitizes untrusted values before embedding in prompts.
|
|
14272
|
+
* Strips newlines and all backticks that could inject instructions,
|
|
14273
|
+
* and truncates to a safe length.
|
|
14274
|
+
*/ function sanitizePromptValue(value) {
|
|
14275
|
+
return value.replace(/[\n\r]/g, " ").replace(/`/g, "").slice(0, 256);
|
|
14276
|
+
}
|
|
14277
|
+
/**
|
|
14278
|
+
* Shell metacharacters that indicate compound or piped commands.
|
|
14279
|
+
* Commands containing these are excluded from the allow list because
|
|
14280
|
+
* they could mask injection (e.g. `npm test && curl evil`).
|
|
14281
|
+
*/ const SHELL_METACHAR_PATTERN = /[;|&`><$()\\\n\r]/;
|
|
14282
|
+
/**
|
|
14283
|
+
* Returns true if the command is non-empty, contains no shell metacharacters,
|
|
14284
|
+
* and is safe to include in a policy allow list.
|
|
14285
|
+
*/ function isSafeCommand(command) {
|
|
14286
|
+
const normalized = command.trim();
|
|
14287
|
+
if (normalized.length === 0) {
|
|
14288
|
+
return false;
|
|
14289
|
+
}
|
|
14290
|
+
return !SHELL_METACHAR_PATTERN.test(normalized);
|
|
14291
|
+
}
|
|
14315
14292
|
|
|
14316
|
-
|
|
14317
|
-
const baseFields = {
|
|
14318
|
-
v: literal(1),
|
|
14319
|
-
session_id: schemas_string(),
|
|
14320
|
-
command: schemas_string(),
|
|
14321
|
-
actor: schemas_string(),
|
|
14322
|
-
timestamp: schemas_string(),
|
|
14323
|
-
env: record(schemas_string(), schemas_string()),
|
|
14324
|
-
tags: record(schemas_string(), schemas_string())
|
|
14325
|
-
};
|
|
14326
|
-
const ScriptEndEventSchema = schemas_object({
|
|
14327
|
-
...baseFields,
|
|
14328
|
-
event: literal("script_end"),
|
|
14329
|
-
script: schemas_string().optional(),
|
|
14330
|
-
package: schemas_string().optional(),
|
|
14331
|
-
package_version: schemas_string().optional(),
|
|
14332
|
-
exit_code: schemas_number().int(),
|
|
14333
|
-
signal: schemas_string().nullable(),
|
|
14334
|
-
duration_ms: schemas_number()
|
|
14335
|
-
}).strict();
|
|
14336
|
-
const ShimErrorEventSchema = schemas_object({
|
|
14337
|
-
...baseFields,
|
|
14338
|
-
event: literal("shim_error")
|
|
14339
|
-
}).strict();
|
|
14340
|
-
const PolicyDecisionEventSchema = schemas_object({
|
|
14341
|
-
...baseFields,
|
|
14342
|
-
event: literal("policy_decision"),
|
|
14343
|
-
decision: schemas_enum([
|
|
14344
|
-
"allow",
|
|
14345
|
-
"deny"
|
|
14346
|
-
]),
|
|
14347
|
-
matched_rule: schemas_string().nullable()
|
|
14348
|
-
}).strict();
|
|
14349
|
-
const ScriptEventSchema = discriminatedUnion("event", [
|
|
14350
|
-
ScriptEndEventSchema,
|
|
14351
|
-
ShimErrorEventSchema,
|
|
14352
|
-
PolicyDecisionEventSchema
|
|
14353
|
-
]);
|
|
14354
|
-
const MAX_POLICY_RULES = 10_000;
|
|
14355
|
-
const MAX_RULE_LENGTH = 1024;
|
|
14356
|
-
const policyRuleArray = schemas_array(schemas_string().max(MAX_RULE_LENGTH)).max(MAX_POLICY_RULES);
|
|
14357
|
-
const PolicyConfigSchema = schemas_object({
|
|
14358
|
-
allow: policyRuleArray.optional(),
|
|
14359
|
-
deny: policyRuleArray.default(()=>[])
|
|
14360
|
-
}).strict();
|
|
14361
|
-
// NOTE: This schema mirrors CopilotHookCommandSchema in @lousy-agents/core
|
|
14362
|
-
// (packages/core/src/entities/copilot-hook-schema.ts). Keep them aligned.
|
|
14363
|
-
// agent-shell cannot import from core since it is a standalone published binary.
|
|
14364
|
-
const MAX_HOOKS_PER_EVENT = 100;
|
|
14365
|
-
/** Regex that allows standard env var names and rejects __proto__ (the prototype-polluting key). */ const ENV_KEY_PATTERN = /^(?!__proto__$)[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
14366
|
-
const HookCommandSchema = schemas_object({
|
|
14367
|
-
type: literal("command"),
|
|
14368
|
-
bash: schemas_string().min(1, "Hook bash command must not be empty").optional(),
|
|
14369
|
-
powershell: schemas_string().min(1, "Hook PowerShell command must not be empty").optional(),
|
|
14370
|
-
cwd: schemas_string().optional(),
|
|
14371
|
-
timeoutSec: schemas_number().positive().optional(),
|
|
14372
|
-
env: record(schemas_string().regex(ENV_KEY_PATTERN, "Hook env key must be a valid identifier (no prototype-polluting keys)"), schemas_string()).optional()
|
|
14373
|
-
}).strict().refine((data)=>Boolean(data.bash) || Boolean(data.powershell), {
|
|
14374
|
-
message: "At least one of 'bash' or 'powershell' must be provided and non-empty"
|
|
14375
|
-
});
|
|
14376
|
-
const hookArray = schemas_array(HookCommandSchema).max(MAX_HOOKS_PER_EVENT);
|
|
14377
|
-
const HooksConfigSchema = schemas_object({
|
|
14378
|
-
version: literal(1),
|
|
14379
|
-
hooks: schemas_object({
|
|
14380
|
-
sessionStart: hookArray.optional(),
|
|
14381
|
-
userPromptSubmitted: hookArray.optional(),
|
|
14382
|
-
preToolUse: hookArray.optional(),
|
|
14383
|
-
postToolUse: hookArray.optional(),
|
|
14384
|
-
sessionEnd: hookArray.optional()
|
|
14385
|
-
}).strict()
|
|
14386
|
-
}).strict();
|
|
14293
|
+
;// CONCATENATED MODULE: ./src/copilot-prompt.ts
|
|
14387
14294
|
|
|
14388
|
-
|
|
14389
|
-
|
|
14295
|
+
function formatScriptsSummary(scripts) {
|
|
14296
|
+
return scripts.map((s)=>` - \`${sanitizePromptValue(s.name)}\`: \`${sanitizePromptValue(s.command)}\``).join("\n");
|
|
14297
|
+
}
|
|
14298
|
+
function formatWorkflowSummary(commands) {
|
|
14299
|
+
return commands.map((cmd)=>` - \`${sanitizePromptValue(cmd)}\``).join("\n");
|
|
14300
|
+
}
|
|
14301
|
+
function formatMiseTasksSummary(tasks) {
|
|
14302
|
+
return tasks.map((t)=>` - \`${sanitizePromptValue(t.name)}\`: \`${sanitizePromptValue(t.command)}\``).join("\n");
|
|
14303
|
+
}
|
|
14304
|
+
/**
|
|
14305
|
+
* Builds the system message that establishes persistent behavioral
|
|
14306
|
+
* constraints for the Copilot SDK session. This includes security goals,
|
|
14307
|
+
* tool descriptions, and response format requirements — context that
|
|
14308
|
+
* must be respected across all tool interactions during the session.
|
|
14309
|
+
*/ function buildSystemMessage() {
|
|
14310
|
+
return `You are a security-focused policy analyst for agent-shell — a security layer that intercepts terminal commands executed by AI coding agents. Your goal is to generate a minimal, secure allow list for a \`policy.json\` file.
|
|
14390
14311
|
|
|
14312
|
+
## Security Principles
|
|
14391
14313
|
|
|
14314
|
+
- Only commands genuinely needed for development workflows should be permitted
|
|
14315
|
+
- Overly broad rules create security risks (e.g. an agent could chain \`npm test && curl evil.com\`)
|
|
14316
|
+
- Use exact commands — avoid wildcards unless the command is genuinely read-only (e.g. \`git status *\`)
|
|
14317
|
+
- Always validate proposed commands with \`validate_allow_rule\` before including them
|
|
14318
|
+
- Commands containing shell metacharacters (\`;\`, \`|\`, \`&\`, \`\`\`, \`$\`, etc.) are never safe
|
|
14392
14319
|
|
|
14393
|
-
|
|
14394
|
-
|
|
14395
|
-
|
|
14396
|
-
|
|
14397
|
-
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14320
|
+
## Available Tools
|
|
14321
|
+
|
|
14322
|
+
### MCP Tools (lousy-agents server)
|
|
14323
|
+
|
|
14324
|
+
- **discover_feedback_loops**: Discover npm scripts and CLI tools from workflows, mapped to SDLC phases (test, build, lint, format, security). Returns structured results grouped by phase. **Start here.**
|
|
14325
|
+
- **discover_environment**: Discover environment config files (mise.toml, .nvmrc, .python-version, etc.) and detect package managers.
|
|
14326
|
+
|
|
14327
|
+
### Custom Tools
|
|
14328
|
+
|
|
14329
|
+
- **read_project_file**: Read any file in the repository (truncated at 100KB). Use to inspect configs discovered via MCP tools.
|
|
14330
|
+
- **validate_allow_rule**: Check whether a proposed command is safe for the allow list (rejects commands containing shell metacharacters).
|
|
14331
|
+
|
|
14332
|
+
## Response Format
|
|
14333
|
+
|
|
14334
|
+
After exploring, respond with **only** a JSON object matching this exact schema — no markdown fences, no explanation:
|
|
14335
|
+
|
|
14336
|
+
\`\`\`json
|
|
14337
|
+
{
|
|
14338
|
+
"additionalAllowRules": ["<command>", ...],
|
|
14339
|
+
"suggestions": ["<human-readable suggestion>", ...]
|
|
14412
14340
|
}
|
|
14413
|
-
|
|
14414
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14341
|
+
\`\`\`
|
|
14342
|
+
|
|
14343
|
+
- \`additionalAllowRules\`: string array of specific commands to add to the allow list
|
|
14344
|
+
- \`suggestions\`: string array of human-readable observations or recommendations about the policy`;
|
|
14345
|
+
}
|
|
14346
|
+
/**
|
|
14347
|
+
* Builds the user prompt containing project-specific scan results
|
|
14348
|
+
* and Socratic questions to guide exploration.
|
|
14349
|
+
*/ function buildAnalysisPrompt(scanResult, repoRoot) {
|
|
14350
|
+
const scriptsList = scanResult.scripts.length > 0 ? formatScriptsSummary(scanResult.scripts) : " (none found)";
|
|
14351
|
+
const workflowList = scanResult.workflowCommands.length > 0 ? formatWorkflowSummary(scanResult.workflowCommands) : " (none found)";
|
|
14352
|
+
const miseList = scanResult.miseTasks.length > 0 ? formatMiseTasksSummary(scanResult.miseTasks) : " (none found)";
|
|
14353
|
+
const languagesList = scanResult.languages.length > 0 ? scanResult.languages.join(", ") : "(none detected)";
|
|
14354
|
+
const safeRepoRoot = sanitizePromptValue(repoRoot);
|
|
14355
|
+
return `# Project Analysis Request
|
|
14356
|
+
|
|
14357
|
+
## Static Analysis Results
|
|
14358
|
+
|
|
14359
|
+
We have already discovered the following from static file analysis:
|
|
14360
|
+
|
|
14361
|
+
- **Repository root**: \`${safeRepoRoot}\`
|
|
14362
|
+
- **Detected languages**: ${languagesList}
|
|
14363
|
+
|
|
14364
|
+
### npm scripts (from package.json)
|
|
14365
|
+
${scriptsList}
|
|
14366
|
+
|
|
14367
|
+
### GitHub Actions workflow commands
|
|
14368
|
+
${workflowList}
|
|
14369
|
+
|
|
14370
|
+
### Mise tasks (from mise.toml)
|
|
14371
|
+
${miseList}
|
|
14372
|
+
|
|
14373
|
+
## Questions to Explore
|
|
14374
|
+
|
|
14375
|
+
1. Call \`discover_feedback_loops\` to get a structured view of project commands mapped to SDLC phases. Are there commands the static analysis missed?
|
|
14376
|
+
2. Call \`discover_environment\` to understand the runtime setup. What toolchain commands does the environment require?
|
|
14377
|
+
3. Given the detected languages (${languagesList}), what language-specific toolchain commands (e.g. \`cargo test\`, \`go build\`, \`pip install\`) might be needed but aren't captured above?
|
|
14378
|
+
4. Are there any commands in the discovered lists that look suspicious or overly broad for a development workflow?
|
|
14379
|
+
5. Use \`validate_allow_rule\` to verify each proposed command before including it in your response.`;
|
|
14380
|
+
}
|
|
14381
|
+
|
|
14382
|
+
;// CONCATENATED MODULE: external "node:fs"
|
|
14383
|
+
const external_node_fs_namespaceObject = __rspack_createRequire_require("node:fs");
|
|
14384
|
+
;// CONCATENATED MODULE: external "node:module"
|
|
14385
|
+
const external_node_module_namespaceObject = __rspack_createRequire_require("node:module");
|
|
14386
|
+
;// CONCATENATED MODULE: external "node:url"
|
|
14387
|
+
const external_node_url_namespaceObject = __rspack_createRequire_require("node:url");
|
|
14388
|
+
;// CONCATENATED MODULE: ./src/resolve-sdk.ts
|
|
14389
|
+
|
|
14390
|
+
|
|
14391
|
+
|
|
14392
|
+
|
|
14393
|
+
/**
|
|
14394
|
+
* Resolves an npm package's ESM entry point from the user's project directory.
|
|
14395
|
+
*
|
|
14396
|
+
* Bundled CLIs resolve bare specifiers relative to the bundle location, not
|
|
14397
|
+
* the user's working directory. This uses `createRequire` anchored at the
|
|
14398
|
+
* project root to locate the package, then reads its `package.json` exports
|
|
14399
|
+
* map to select the ESM entry that `import()` would use.
|
|
14400
|
+
*
|
|
14401
|
+
* @returns A file:// URL to the package ESM entry point, or null if not found
|
|
14402
|
+
*/ function resolveSdkPath(repoRoot, packageName) {
|
|
14403
|
+
if (!repoRoot || !(0,external_node_path_namespaceObject.isAbsolute)(repoRoot)) return null;
|
|
14404
|
+
if (/^[./]/.test(packageName) || packageName.includes("..")) return null;
|
|
14405
|
+
try {
|
|
14406
|
+
const projectRequire = (0,external_node_module_namespaceObject.createRequire)((0,external_node_path_namespaceObject.resolve)(repoRoot, "package.json"));
|
|
14407
|
+
const cjsResolved = projectRequire.resolve(packageName);
|
|
14408
|
+
const esmUrl = findEsmEntry(cjsResolved, packageName);
|
|
14409
|
+
return esmUrl ?? (0,external_node_url_namespaceObject.pathToFileURL)(cjsResolved).href;
|
|
14410
|
+
} catch {
|
|
14411
|
+
return null;
|
|
14412
|
+
}
|
|
14413
|
+
}
|
|
14414
|
+
/**
|
|
14415
|
+
* Walks up from a resolved module path to find the package root directory,
|
|
14416
|
+
* then extracts the ESM entry point from its exports map.
|
|
14417
|
+
*/ function findEsmEntry(resolvedPath, packageName) {
|
|
14418
|
+
let dir = (0,external_node_path_namespaceObject.dirname)(resolvedPath);
|
|
14419
|
+
while(true){
|
|
14427
14420
|
try {
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
if (
|
|
14431
|
-
|
|
14432
|
-
|
|
14433
|
-
|
|
14434
|
-
|
|
14421
|
+
const raw = (0,external_node_fs_namespaceObject.readFileSync)((0,external_node_path_namespaceObject.join)(dir, "package.json"), "utf-8");
|
|
14422
|
+
const pkg = JSON.parse(raw);
|
|
14423
|
+
if (typeof pkg === "object" && pkg !== null && pkg.name === packageName) {
|
|
14424
|
+
const esmEntry = extractEsmEntry(pkg);
|
|
14425
|
+
const full = (0,external_node_path_namespaceObject.resolve)(dir, esmEntry);
|
|
14426
|
+
try {
|
|
14427
|
+
return (0,external_node_url_namespaceObject.pathToFileURL)((0,external_node_fs_namespaceObject.realpathSync)(full)).href;
|
|
14428
|
+
} catch {
|
|
14429
|
+
return null;
|
|
14430
|
+
}
|
|
14435
14431
|
}
|
|
14436
|
-
|
|
14437
|
-
}
|
|
14438
|
-
|
|
14439
|
-
|
|
14440
|
-
|
|
14441
|
-
error: "AGENTSHELL_LOG_DIR resolves outside project root"
|
|
14442
|
-
};
|
|
14443
|
-
}
|
|
14444
|
-
return {
|
|
14445
|
-
dir: resolved
|
|
14446
|
-
};
|
|
14432
|
+
} catch {
|
|
14433
|
+
/* no package.json at this level */ }
|
|
14434
|
+
const parent = (0,external_node_path_namespaceObject.dirname)(dir);
|
|
14435
|
+
if (parent === dir) return null;
|
|
14436
|
+
dir = parent;
|
|
14447
14437
|
}
|
|
14448
|
-
return {
|
|
14449
|
-
dir: defaultDir
|
|
14450
|
-
};
|
|
14451
14438
|
}
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
|
|
14455
|
-
|
|
14456
|
-
|
|
14457
|
-
|
|
14439
|
+
/**
|
|
14440
|
+
* Extracts the ESM entry point from a parsed package.json object.
|
|
14441
|
+
*/ function extractEsmEntry(pkgJson) {
|
|
14442
|
+
if (typeof pkgJson === "object" && pkgJson !== null) {
|
|
14443
|
+
const pkg = pkgJson;
|
|
14444
|
+
const esmPath = resolveExportsImportEntry(pkg.exports);
|
|
14445
|
+
if (esmPath) return esmPath;
|
|
14446
|
+
if (typeof pkg.main === "string" && pkg.main.length > 0) return pkg.main;
|
|
14458
14447
|
}
|
|
14459
|
-
return
|
|
14448
|
+
return "index.js";
|
|
14460
14449
|
}
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
|
|
14464
|
-
|
|
14465
|
-
|
|
14466
|
-
|
|
14467
|
-
|
|
14468
|
-
if (
|
|
14469
|
-
|
|
14450
|
+
/**
|
|
14451
|
+
* Navigates the exports map to find the ESM entry. Supports string-shaped
|
|
14452
|
+
* exports, string-valued `exports["."]`, `exports["."].import` (string),
|
|
14453
|
+
* `exports["."].import.default`, and the sugar form where condition keys
|
|
14454
|
+
* (`import`/`require`) appear directly on the exports object.
|
|
14455
|
+
*/ function resolveExportsImportEntry(exports) {
|
|
14456
|
+
if (typeof exports === "string") return exports;
|
|
14457
|
+
if (typeof exports !== "object" || exports === null) return null;
|
|
14458
|
+
const exportsMap = exports;
|
|
14459
|
+
const dotEntry = exportsMap["."];
|
|
14460
|
+
if (typeof dotEntry === "string") return dotEntry;
|
|
14461
|
+
const conditionSource = typeof dotEntry === "object" && dotEntry !== null && !Array.isArray(dotEntry) ? dotEntry : exportsMap;
|
|
14462
|
+
const importEntry = conditionSource.import;
|
|
14463
|
+
if (typeof importEntry === "string") return importEntry;
|
|
14464
|
+
if (typeof importEntry === "object" && importEntry !== null) {
|
|
14465
|
+
const importMap = importEntry;
|
|
14466
|
+
if (typeof importMap.default === "string") return importMap.default;
|
|
14470
14467
|
}
|
|
14471
|
-
|
|
14472
|
-
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14468
|
+
return null;
|
|
14469
|
+
}
|
|
14470
|
+
|
|
14471
|
+
;// CONCATENATED MODULE: ./src/copilot-enhance.ts
|
|
14472
|
+
|
|
14473
|
+
|
|
14474
|
+
|
|
14475
|
+
|
|
14476
|
+
|
|
14477
|
+
|
|
14478
|
+
const DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
14479
|
+
const MAX_FILE_READ_BYTES = 102_400;
|
|
14480
|
+
const AnalysisResponseSchema = schemas_object({
|
|
14481
|
+
additionalAllowRules: schemas_array(schemas_string().max(512)).max(100),
|
|
14482
|
+
suggestions: schemas_array(schemas_string().max(1024)).max(100)
|
|
14483
|
+
});
|
|
14484
|
+
/**
|
|
14485
|
+
* Resolves a relative path against a root directory and verifies it
|
|
14486
|
+
* does not escape the root via traversal (e.g. `../../etc/passwd`).
|
|
14487
|
+
*
|
|
14488
|
+
* @returns The resolved absolute path, or null if it escapes the root
|
|
14489
|
+
*/ function resolveSafePath(repoRoot, relativePath) {
|
|
14490
|
+
const root = repoRoot.replace(/\/+$/, "") || "/";
|
|
14491
|
+
const resolved = (0,external_node_path_namespaceObject.resolve)(root, (0,external_node_path_namespaceObject.normalize)(relativePath));
|
|
14492
|
+
const prefix = root === "/" ? "/" : `${root}/`;
|
|
14493
|
+
if (!resolved.startsWith(prefix) && resolved !== root) {
|
|
14494
|
+
return null;
|
|
14476
14495
|
}
|
|
14477
|
-
return
|
|
14496
|
+
return resolved;
|
|
14478
14497
|
}
|
|
14479
|
-
|
|
14480
|
-
|
|
14481
|
-
|
|
14498
|
+
/**
|
|
14499
|
+
* Finds the largest byte offset ≤ maxBytes that falls on a clean UTF-8
|
|
14500
|
+
* codepoint boundary within the given buffer.
|
|
14501
|
+
*
|
|
14502
|
+
* Prevents splitting multi-byte sequences when truncating raw buffers.
|
|
14503
|
+
*/ function findUtf8Boundary(buf, maxBytes) {
|
|
14504
|
+
if (maxBytes >= buf.length) return buf.length;
|
|
14505
|
+
if (maxBytes === 0) return 0;
|
|
14506
|
+
// Check the byte at the cut point (first excluded byte).
|
|
14507
|
+
// If it's not a continuation byte (10xxxxxx), maxBytes is already a
|
|
14508
|
+
// clean boundary — the previous character ended before this position.
|
|
14509
|
+
if ((buf[maxBytes] & 0xc0) !== 0x80) {
|
|
14510
|
+
return maxBytes;
|
|
14511
|
+
}
|
|
14512
|
+
// The first excluded byte is a continuation byte, so we're splitting
|
|
14513
|
+
// a multi-byte character. Walk backwards to find the start byte.
|
|
14514
|
+
let boundary = maxBytes;
|
|
14515
|
+
while(boundary > 0 && (buf[boundary - 1] & 0xc0) === 0x80){
|
|
14516
|
+
boundary--;
|
|
14517
|
+
}
|
|
14518
|
+
// boundary-1 is now the start byte of the incomplete character.
|
|
14519
|
+
// Exclude it since its full sequence extends past maxBytes.
|
|
14520
|
+
if (boundary > 0) {
|
|
14521
|
+
boundary--;
|
|
14522
|
+
}
|
|
14523
|
+
return boundary;
|
|
14524
|
+
}
|
|
14525
|
+
/**
|
|
14526
|
+
* Reads a project file safely, enforcing path traversal protection
|
|
14527
|
+
* and byte-level truncation. Extracted for testability.
|
|
14528
|
+
*/ async function readProjectFileSafe(repoRoot, pathArg) {
|
|
14529
|
+
if (pathArg.length === 0) {
|
|
14530
|
+
return {
|
|
14531
|
+
error: "Path is required"
|
|
14532
|
+
};
|
|
14482
14533
|
}
|
|
14483
|
-
|
|
14534
|
+
const safePath = resolveSafePath(repoRoot, pathArg);
|
|
14535
|
+
if (safePath === null) {
|
|
14536
|
+
return {
|
|
14537
|
+
error: "Path is outside the repository"
|
|
14538
|
+
};
|
|
14539
|
+
}
|
|
14540
|
+
let fileBuffer;
|
|
14484
14541
|
try {
|
|
14485
|
-
|
|
14542
|
+
const root = repoRoot.replace(/\/+$/, "") || "/";
|
|
14543
|
+
const [realRoot, realPath] = await Promise.all([
|
|
14544
|
+
(0,promises_namespaceObject.realpath)(root),
|
|
14545
|
+
(0,promises_namespaceObject.realpath)(safePath)
|
|
14546
|
+
]);
|
|
14547
|
+
const realPrefix = realRoot === "/" ? "/" : `${realRoot}/`;
|
|
14548
|
+
if (!realPath.startsWith(realPrefix) && realPath !== realRoot) {
|
|
14549
|
+
return {
|
|
14550
|
+
error: "Path is outside the repository"
|
|
14551
|
+
};
|
|
14552
|
+
}
|
|
14553
|
+
fileBuffer = await (0,promises_namespaceObject.readFile)(realPath);
|
|
14486
14554
|
} catch {
|
|
14487
|
-
return
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
if (!result.success) {
|
|
14491
|
-
return undefined;
|
|
14555
|
+
return {
|
|
14556
|
+
error: "File not found or unreadable"
|
|
14557
|
+
};
|
|
14492
14558
|
}
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
const allFiles = await deps.readdir(eventsDir);
|
|
14497
|
-
const jsonlFiles = allFiles.filter((f)=>f.endsWith(".jsonl"));
|
|
14498
|
-
if (jsonlFiles.length === 0) {
|
|
14559
|
+
// Buffer processing outside I/O catch — programming bugs in
|
|
14560
|
+
// findUtf8Boundary propagate instead of being silently swallowed.
|
|
14561
|
+
if (fileBuffer.length <= MAX_FILE_READ_BYTES) {
|
|
14499
14562
|
return {
|
|
14500
|
-
|
|
14501
|
-
|
|
14563
|
+
content: fileBuffer.toString("utf-8"),
|
|
14564
|
+
truncated: false
|
|
14502
14565
|
};
|
|
14503
14566
|
}
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
14535
|
-
|
|
14567
|
+
const boundary = findUtf8Boundary(fileBuffer, MAX_FILE_READ_BYTES);
|
|
14568
|
+
return {
|
|
14569
|
+
content: fileBuffer.subarray(0, boundary).toString("utf-8"),
|
|
14570
|
+
truncated: true
|
|
14571
|
+
};
|
|
14572
|
+
}
|
|
14573
|
+
/**
|
|
14574
|
+
* Creates custom tools that allow the Copilot model to read files
|
|
14575
|
+
* and validate proposed allow rules. Structured project discovery
|
|
14576
|
+
* is handled by the lousy-agents MCP server (connected via mcpServers).
|
|
14577
|
+
*/ function createCustomTools(repoRoot, defineTool) {
|
|
14578
|
+
const readProjectFile = defineTool("read_project_file", {
|
|
14579
|
+
description: "Read a file from the project repository. Returns file content (truncated at 100KB). Use to inspect build configs, Dockerfiles, Makefiles, etc.",
|
|
14580
|
+
parameters: {
|
|
14581
|
+
type: "object",
|
|
14582
|
+
properties: {
|
|
14583
|
+
path: {
|
|
14584
|
+
type: "string",
|
|
14585
|
+
description: "Relative path from repository root"
|
|
14586
|
+
}
|
|
14587
|
+
},
|
|
14588
|
+
required: [
|
|
14589
|
+
"path"
|
|
14590
|
+
]
|
|
14591
|
+
},
|
|
14592
|
+
skipPermission: true,
|
|
14593
|
+
handler: (args)=>readProjectFileSafe(repoRoot, args.path ?? "")
|
|
14594
|
+
});
|
|
14595
|
+
const validateAllowRule = defineTool("validate_allow_rule", {
|
|
14596
|
+
description: "Check whether a proposed allow rule is safe (does not contain shell metacharacters like ;, |, &, `, $, etc.).",
|
|
14597
|
+
parameters: {
|
|
14598
|
+
type: "object",
|
|
14599
|
+
properties: {
|
|
14600
|
+
command: {
|
|
14601
|
+
type: "string",
|
|
14602
|
+
description: "The command to validate as a policy rule"
|
|
14603
|
+
}
|
|
14604
|
+
},
|
|
14605
|
+
required: [
|
|
14606
|
+
"command"
|
|
14607
|
+
]
|
|
14608
|
+
},
|
|
14609
|
+
skipPermission: true,
|
|
14610
|
+
handler: async (args)=>{
|
|
14611
|
+
const command = args.command ?? "";
|
|
14612
|
+
if (isSafeCommand(command)) {
|
|
14613
|
+
return {
|
|
14614
|
+
safe: true,
|
|
14615
|
+
reason: "No shell metacharacters detected"
|
|
14616
|
+
};
|
|
14536
14617
|
}
|
|
14537
|
-
|
|
14538
|
-
|
|
14618
|
+
const normalized = command.trim();
|
|
14619
|
+
if (normalized.length === 0) {
|
|
14620
|
+
return {
|
|
14621
|
+
safe: false,
|
|
14622
|
+
reason: "Empty or whitespace-only commands are not allowed in policy rules"
|
|
14623
|
+
};
|
|
14539
14624
|
}
|
|
14625
|
+
return {
|
|
14626
|
+
safe: false,
|
|
14627
|
+
reason: "Contains shell metacharacters — compound commands are not allowed in policy rules"
|
|
14628
|
+
};
|
|
14540
14629
|
}
|
|
14541
|
-
}
|
|
14542
|
-
return
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
|
|
14630
|
+
});
|
|
14631
|
+
return [
|
|
14632
|
+
readProjectFile,
|
|
14633
|
+
validateAllowRule
|
|
14634
|
+
];
|
|
14546
14635
|
}
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
14555
|
-
|
|
14556
|
-
|
|
14557
|
-
|
|
14558
|
-
|
|
14559
|
-
|
|
14560
|
-
|
|
14561
|
-
|
|
14562
|
-
|
|
14636
|
+
/**
|
|
14637
|
+
* Attempts to use the @github/copilot-sdk to enhance policy generation
|
|
14638
|
+
* with AI-powered project analysis. Connects to the lousy-agents MCP
|
|
14639
|
+
* server for structured project discovery (feedback loops, environment)
|
|
14640
|
+
* and provides custom tools for file reading and rule validation.
|
|
14641
|
+
* Falls back gracefully if the SDK or Copilot CLI is not available.
|
|
14642
|
+
*
|
|
14643
|
+
* @returns Enhanced analysis results, or null if the SDK is unavailable
|
|
14644
|
+
*/ async function enhanceWithCopilot(scanResult, repoRoot, writeStderr, model = DEFAULT_MODEL) {
|
|
14645
|
+
let importSucceeded = false;
|
|
14646
|
+
try {
|
|
14647
|
+
const sdkPath = resolveSdkPath(repoRoot, "@github/copilot-sdk");
|
|
14648
|
+
const { CopilotClient, defineTool, approveAll } = sdkPath ? await import(/* webpackIgnore: true */ sdkPath) : await __webpack_require__.e(/* import() */ "300").then(__webpack_require__.bind(__webpack_require__, 791));
|
|
14649
|
+
importSucceeded = true;
|
|
14650
|
+
const client = new CopilotClient();
|
|
14651
|
+
const tools = createCustomTools(repoRoot, defineTool);
|
|
14652
|
+
try {
|
|
14653
|
+
await client.start();
|
|
14654
|
+
const session = await client.createSession({
|
|
14655
|
+
model,
|
|
14656
|
+
tools,
|
|
14657
|
+
onPermissionRequest: approveAll,
|
|
14658
|
+
systemMessage: {
|
|
14659
|
+
content: buildSystemMessage()
|
|
14660
|
+
},
|
|
14661
|
+
mcpServers: {
|
|
14662
|
+
"lousy-agents": {
|
|
14663
|
+
type: "local",
|
|
14664
|
+
command: "npx",
|
|
14665
|
+
args: [
|
|
14666
|
+
"-y",
|
|
14667
|
+
"@lousy-agents/mcp"
|
|
14668
|
+
],
|
|
14669
|
+
cwd: repoRoot,
|
|
14670
|
+
tools: [
|
|
14671
|
+
"discover_feedback_loops",
|
|
14672
|
+
"discover_environment"
|
|
14673
|
+
]
|
|
14674
|
+
}
|
|
14675
|
+
}
|
|
14676
|
+
});
|
|
14563
14677
|
try {
|
|
14564
|
-
|
|
14565
|
-
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
|
|
14678
|
+
const prompt = buildAnalysisPrompt(scanResult, repoRoot);
|
|
14679
|
+
const response = await session.sendAndWait({
|
|
14680
|
+
prompt
|
|
14681
|
+
});
|
|
14682
|
+
const data = typeof response?.data === "object" && response.data !== null ? response.data : undefined;
|
|
14683
|
+
const content = data !== undefined && "content" in data && typeof data.content === "string" ? data.content : "";
|
|
14684
|
+
return parseAnalysisResponse(content);
|
|
14685
|
+
} finally{
|
|
14686
|
+
await session.disconnect();
|
|
14571
14687
|
}
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14688
|
+
} finally{
|
|
14689
|
+
await client.stop();
|
|
14690
|
+
}
|
|
14691
|
+
} catch (err) {
|
|
14692
|
+
if (process.env.AGENT_SHELL_COPILOT_DEBUG) {
|
|
14693
|
+
if (importSucceeded) {
|
|
14694
|
+
writeStderr(`agent-shell: Copilot analysis failed — ${sanitizeForStderr(err)}\n`);
|
|
14695
|
+
} else {
|
|
14696
|
+
writeStderr("agent-shell: Copilot SDK not available — using static analysis only\n");
|
|
14577
14697
|
}
|
|
14578
|
-
|
|
14579
|
-
|
|
14698
|
+
}
|
|
14699
|
+
return null;
|
|
14700
|
+
}
|
|
14701
|
+
}
|
|
14702
|
+
/**
|
|
14703
|
+
* Finds the first well-formed JSON object in `content` using brace-balancing,
|
|
14704
|
+
* rather than a greedy regex that could capture extra trailing braces and
|
|
14705
|
+
* fail JSON.parse on valid responses that include preamble or postamble text.
|
|
14706
|
+
*/ function extractFirstJsonObject(content) {
|
|
14707
|
+
const start = content.indexOf("{");
|
|
14708
|
+
if (start === -1) {
|
|
14709
|
+
return null;
|
|
14710
|
+
}
|
|
14711
|
+
let inString = false;
|
|
14712
|
+
let escaping = false;
|
|
14713
|
+
let depth = 0;
|
|
14714
|
+
for(let i = start; i < content.length; i += 1){
|
|
14715
|
+
const ch = content[i];
|
|
14716
|
+
if (escaping) {
|
|
14717
|
+
escaping = false;
|
|
14718
|
+
continue;
|
|
14719
|
+
}
|
|
14720
|
+
if (ch === "\\") {
|
|
14721
|
+
if (inString) {
|
|
14722
|
+
escaping = true;
|
|
14580
14723
|
}
|
|
14724
|
+
continue;
|
|
14581
14725
|
}
|
|
14582
|
-
if (
|
|
14583
|
-
|
|
14584
|
-
|
|
14585
|
-
|
|
14586
|
-
|
|
14587
|
-
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
|
|
14591
|
-
|
|
14726
|
+
if (ch === '"') {
|
|
14727
|
+
inString = !inString;
|
|
14728
|
+
continue;
|
|
14729
|
+
}
|
|
14730
|
+
if (!inString) {
|
|
14731
|
+
if (ch === "{") {
|
|
14732
|
+
depth += 1;
|
|
14733
|
+
} else if (ch === "}") {
|
|
14734
|
+
depth -= 1;
|
|
14735
|
+
if (depth === 0) {
|
|
14736
|
+
return content.slice(start, i + 1);
|
|
14737
|
+
}
|
|
14738
|
+
}
|
|
14592
14739
|
}
|
|
14593
14740
|
}
|
|
14594
|
-
|
|
14595
|
-
|
|
14741
|
+
return null;
|
|
14742
|
+
}
|
|
14743
|
+
function parseAnalysisResponse(content) {
|
|
14744
|
+
const jsonText = extractFirstJsonObject(content);
|
|
14745
|
+
if (!jsonText) {
|
|
14746
|
+
return null;
|
|
14747
|
+
}
|
|
14748
|
+
try {
|
|
14749
|
+
const parsed = JSON.parse(jsonText);
|
|
14750
|
+
return AnalysisResponseSchema.parse(parsed);
|
|
14751
|
+
} catch {
|
|
14752
|
+
return null;
|
|
14753
|
+
}
|
|
14596
14754
|
}
|
|
14597
14755
|
|
|
14598
|
-
;// CONCATENATED MODULE: ./src/
|
|
14599
|
-
|
|
14600
|
-
|
|
14601
|
-
|
|
14756
|
+
;// CONCATENATED MODULE: ./src/project-scanner.ts
|
|
14602
14757
|
|
|
14603
14758
|
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
options.errors.push("--actor requires a value");
|
|
14628
|
-
}
|
|
14629
|
-
break;
|
|
14630
|
-
case "--failures":
|
|
14631
|
-
options.failures = true;
|
|
14632
|
-
break;
|
|
14633
|
-
case "--script":
|
|
14634
|
-
if (i + 1 < args.length) {
|
|
14635
|
-
options.script = args[++i];
|
|
14636
|
-
} else {
|
|
14637
|
-
options.errors = options.errors ?? [];
|
|
14638
|
-
options.errors.push("--script requires a value");
|
|
14639
|
-
}
|
|
14640
|
-
break;
|
|
14641
|
-
case "--list-sessions":
|
|
14642
|
-
options.listSessions = true;
|
|
14643
|
-
break;
|
|
14644
|
-
case "--json":
|
|
14645
|
-
options.json = true;
|
|
14646
|
-
break;
|
|
14647
|
-
}
|
|
14648
|
-
i++;
|
|
14759
|
+
const MAX_WORKFLOW_FILES = 100;
|
|
14760
|
+
const MAX_FILE_SIZE_BYTES = 524_288;
|
|
14761
|
+
const LANGUAGE_MARKERS = {
|
|
14762
|
+
"package.json": "node",
|
|
14763
|
+
"requirements.txt": "python",
|
|
14764
|
+
// biome-ignore lint/style/useNamingConvention: filename on disk
|
|
14765
|
+
Pipfile: "python",
|
|
14766
|
+
"pyproject.toml": "python",
|
|
14767
|
+
"setup.py": "python",
|
|
14768
|
+
"go.mod": "go",
|
|
14769
|
+
"Cargo.toml": "rust",
|
|
14770
|
+
// biome-ignore lint/style/useNamingConvention: filename on disk
|
|
14771
|
+
Gemfile: "ruby",
|
|
14772
|
+
"pom.xml": "java",
|
|
14773
|
+
"build.gradle": "java",
|
|
14774
|
+
"build.gradle.kts": "java"
|
|
14775
|
+
};
|
|
14776
|
+
async function fileExists(path) {
|
|
14777
|
+
try {
|
|
14778
|
+
const s = await (0,promises_namespaceObject.stat)(path);
|
|
14779
|
+
return s.isFile();
|
|
14780
|
+
} catch {
|
|
14781
|
+
return false;
|
|
14649
14782
|
}
|
|
14650
|
-
return options;
|
|
14651
14783
|
}
|
|
14652
|
-
function
|
|
14653
|
-
return {
|
|
14654
|
-
readdir: (path)=>(0,promises_namespaceObject.readdir)(path),
|
|
14655
|
-
stat: (path)=>(0,promises_namespaceObject.stat)(path).then((s)=>({
|
|
14656
|
-
mtimeMs: s.mtimeMs
|
|
14657
|
-
})),
|
|
14658
|
-
realpath: (path)=>(0,promises_namespaceObject.realpath)(path),
|
|
14659
|
-
cwd: ()=>process.cwd(),
|
|
14660
|
-
readFileLines: (path)=>(0,external_node_readline_namespaceObject.createInterface)({
|
|
14661
|
-
input: (0,external_node_fs_namespaceObject.createReadStream)(path, {
|
|
14662
|
-
encoding: "utf-8"
|
|
14663
|
-
})
|
|
14664
|
-
}),
|
|
14665
|
-
writeStderr: (msg)=>{
|
|
14666
|
-
process.stderr.write(msg);
|
|
14667
|
-
}
|
|
14668
|
-
};
|
|
14669
|
-
}
|
|
14670
|
-
async function runLog(args) {
|
|
14671
|
-
const options = parseLogArgs(args);
|
|
14672
|
-
const deps = createDefaultQueryDeps();
|
|
14673
|
-
if (options.errors && options.errors.length > 0) {
|
|
14674
|
-
for (const err of options.errors){
|
|
14675
|
-
process.stderr.write(`agent-shell: ${err}\n`);
|
|
14676
|
-
}
|
|
14677
|
-
return 1;
|
|
14678
|
-
}
|
|
14679
|
-
const { dir, error } = await resolveReadEventsDir(process.env, deps);
|
|
14680
|
-
if (error) {
|
|
14681
|
-
process.stderr.write(`agent-shell: ${error}\n`);
|
|
14682
|
-
return 1;
|
|
14683
|
-
}
|
|
14684
|
-
if (options.last) {
|
|
14685
|
-
try {
|
|
14686
|
-
parseDuration(options.last);
|
|
14687
|
-
} catch (err) {
|
|
14688
|
-
process.stderr.write(`agent-shell: ${err.message}\n`);
|
|
14689
|
-
return 1;
|
|
14690
|
-
}
|
|
14691
|
-
}
|
|
14784
|
+
async function dirExists(path) {
|
|
14692
14785
|
try {
|
|
14693
|
-
await
|
|
14786
|
+
const s = await (0,promises_namespaceObject.stat)(path);
|
|
14787
|
+
return s.isDirectory();
|
|
14694
14788
|
} catch {
|
|
14695
|
-
|
|
14696
|
-
process.stdout.write("To enable instrumentation, add to your .npmrc:\n");
|
|
14697
|
-
process.stdout.write(" script-shell=./node_modules/.bin/agent-shell\n");
|
|
14698
|
-
return 0;
|
|
14699
|
-
}
|
|
14700
|
-
if (options.listSessions) {
|
|
14701
|
-
const sessions = await listSessions(dir, deps);
|
|
14702
|
-
if (sessions.length === 0) {
|
|
14703
|
-
process.stdout.write("No sessions found.\n");
|
|
14704
|
-
return 0;
|
|
14705
|
-
}
|
|
14706
|
-
process.stdout.write(formatSessionsTable(sessions));
|
|
14707
|
-
process.stdout.write("\n");
|
|
14708
|
-
return 0;
|
|
14709
|
-
}
|
|
14710
|
-
const result = await queryEvents(dir, {
|
|
14711
|
-
actor: options.actor,
|
|
14712
|
-
failures: options.failures || undefined,
|
|
14713
|
-
script: options.script,
|
|
14714
|
-
last: options.last
|
|
14715
|
-
}, deps);
|
|
14716
|
-
if (result.events.length === 0) {
|
|
14717
|
-
process.stdout.write("No matching events found.\n");
|
|
14718
|
-
return 0;
|
|
14719
|
-
}
|
|
14720
|
-
if (options.json) {
|
|
14721
|
-
process.stdout.write(formatEventsJson(result.events));
|
|
14722
|
-
process.stdout.write("\n");
|
|
14723
|
-
} else {
|
|
14724
|
-
process.stdout.write(formatEventsTable(result.events));
|
|
14725
|
-
process.stdout.write("\n");
|
|
14789
|
+
return false;
|
|
14726
14790
|
}
|
|
14727
|
-
return 0;
|
|
14728
14791
|
}
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14792
|
+
async function discoverScripts(targetDir) {
|
|
14793
|
+
const packageJsonPath = (0,external_node_path_namespaceObject.join)(targetDir, "package.json");
|
|
14794
|
+
if (!await fileExists(packageJsonPath)) {
|
|
14795
|
+
return [];
|
|
14796
|
+
}
|
|
14797
|
+
try {
|
|
14798
|
+
const fileStat = await (0,promises_namespaceObject.stat)(packageJsonPath);
|
|
14799
|
+
if (fileStat.size > MAX_FILE_SIZE_BYTES) {
|
|
14800
|
+
return [];
|
|
14801
|
+
}
|
|
14802
|
+
const content = await (0,promises_namespaceObject.readFile)(packageJsonPath, "utf-8");
|
|
14803
|
+
const parsed = JSON.parse(content);
|
|
14804
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14805
|
+
return [];
|
|
14806
|
+
}
|
|
14807
|
+
const pkg = parsed;
|
|
14808
|
+
const scripts = pkg.scripts;
|
|
14809
|
+
if (scripts === null || scripts === undefined || typeof scripts !== "object" || Array.isArray(scripts)) {
|
|
14810
|
+
return [];
|
|
14811
|
+
}
|
|
14812
|
+
const result = [];
|
|
14813
|
+
for (const [name, command] of Object.entries(scripts)){
|
|
14814
|
+
if (typeof command === "string") {
|
|
14815
|
+
result.push({
|
|
14816
|
+
name,
|
|
14817
|
+
command
|
|
14818
|
+
});
|
|
14739
14819
|
}
|
|
14740
14820
|
}
|
|
14821
|
+
return result;
|
|
14822
|
+
} catch {
|
|
14823
|
+
return [];
|
|
14741
14824
|
}
|
|
14742
|
-
return options;
|
|
14743
14825
|
}
|
|
14744
|
-
function
|
|
14745
|
-
const
|
|
14746
|
-
if (
|
|
14747
|
-
|
|
14748
|
-
};
|
|
14749
|
-
if (firstArg === "policy" && args[1] === "--init") {
|
|
14750
|
-
const options = parsePolicyInitOptions(args);
|
|
14751
|
-
return {
|
|
14752
|
-
type: "policy-init",
|
|
14753
|
-
model: options.model
|
|
14754
|
-
};
|
|
14826
|
+
async function discoverWorkflowCommands(targetDir) {
|
|
14827
|
+
const workflowsDir = (0,external_node_path_namespaceObject.join)(targetDir, ".github", "workflows");
|
|
14828
|
+
if (!await dirExists(workflowsDir)) {
|
|
14829
|
+
return [];
|
|
14755
14830
|
}
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14831
|
+
let files;
|
|
14832
|
+
try {
|
|
14833
|
+
files = await (0,promises_namespaceObject.readdir)(workflowsDir);
|
|
14834
|
+
} catch {
|
|
14835
|
+
return [];
|
|
14761
14836
|
}
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
}
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
* Escapes ASCII and C1 control characters in error messages before writing
|
|
14780
|
-
* to stderr. Replaces each control character with its `\xNN` hex
|
|
14781
|
-
* representation to prevent log/terminal injection when errors embed
|
|
14782
|
-
* untrusted data.
|
|
14783
|
-
*/ function sanitizeForStderr(err) {
|
|
14784
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
14785
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentionally matching control characters for sanitization
|
|
14786
|
-
return msg.replace(/[\u0000-\u001f\u007f-\u009f]/g, (ch)=>{
|
|
14787
|
-
return `\\x${ch.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
|
14788
|
-
});
|
|
14837
|
+
const yamlFiles = files.filter((f)=>f.endsWith(".yml") || f.endsWith(".yaml")).slice(0, MAX_WORKFLOW_FILES);
|
|
14838
|
+
const allCommands = [];
|
|
14839
|
+
for (const file of yamlFiles){
|
|
14840
|
+
try {
|
|
14841
|
+
const filePath = (0,external_node_path_namespaceObject.join)(workflowsDir, file);
|
|
14842
|
+
const fileStat = await (0,promises_namespaceObject.stat)(filePath);
|
|
14843
|
+
if (fileStat.size > MAX_FILE_SIZE_BYTES) {
|
|
14844
|
+
continue;
|
|
14845
|
+
}
|
|
14846
|
+
const content = await (0,promises_namespaceObject.readFile)(filePath, "utf-8");
|
|
14847
|
+
const commands = extractRunCommandsFromYaml(content);
|
|
14848
|
+
allCommands.push(...commands);
|
|
14849
|
+
} catch {}
|
|
14850
|
+
}
|
|
14851
|
+
return [
|
|
14852
|
+
...new Set(allCommands)
|
|
14853
|
+
].filter(isUsefulCommand);
|
|
14789
14854
|
}
|
|
14790
14855
|
/**
|
|
14791
|
-
*
|
|
14792
|
-
*
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
if (normalized.length === 0) {
|
|
14817
|
-
return false;
|
|
14818
|
-
}
|
|
14819
|
-
return !SHELL_METACHAR_PATTERN.test(normalized);
|
|
14856
|
+
* Patterns that match shell control structures, variable assignments,
|
|
14857
|
+
* and other non-command lines extracted from multi-line workflow `run:`
|
|
14858
|
+
* blocks. These are filtered out because they are not meaningful tool
|
|
14859
|
+
* invocations that should appear in a policy allow list — only actual
|
|
14860
|
+
* commands (npm, mise, node, git, etc.) are useful for policy rules.
|
|
14861
|
+
*/ const SHELL_NOISE_PATTERNS = [
|
|
14862
|
+
/^(if|then|else|elif|fi|for|while|do|done|case|esac)\b/,
|
|
14863
|
+
/^(set\s+[+-]e|set\s+[+-]o)/,
|
|
14864
|
+
/^(exit\s+\d|exit\s+\$)/,
|
|
14865
|
+
/^\w+="?\$\{\{/,
|
|
14866
|
+
/^[A-Z_]+=("[^"]*"|'[^']*'|\S+)\s*\\?$/,
|
|
14867
|
+
/^(echo|printf)\s/,
|
|
14868
|
+
/^(cd|mkdir|rm|test)\s/,
|
|
14869
|
+
/^>\s/,
|
|
14870
|
+
/^(else|fi|done|esac)$/,
|
|
14871
|
+
/^\w+=\$\?$/,
|
|
14872
|
+
/^\\$/,
|
|
14873
|
+
/^'[^']*'\s*\\?$/,
|
|
14874
|
+
/^"[^"]*"\s*\\?$/,
|
|
14875
|
+
/^node\s+"?\$/
|
|
14876
|
+
];
|
|
14877
|
+
function isUsefulCommand(cmd) {
|
|
14878
|
+
if (cmd.length < 3) return false;
|
|
14879
|
+
if (cmd.endsWith(")") && !cmd.includes("(")) return false;
|
|
14880
|
+
return !SHELL_NOISE_PATTERNS.some((pattern)=>pattern.test(cmd));
|
|
14820
14881
|
}
|
|
14821
|
-
|
|
14822
|
-
;// CONCATENATED MODULE: ./src/policy.ts
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
const DEFAULT_POLICY_SUBPATH = ".github/hooks/agent-shell/policy.json";
|
|
14828
14882
|
/**
|
|
14829
|
-
*
|
|
14830
|
-
*
|
|
14831
|
-
*
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
return false;
|
|
14845
|
-
}
|
|
14846
|
-
let cursor = prefixSegment.length;
|
|
14847
|
-
for (const segment of innerSegments){
|
|
14848
|
-
const index = command.indexOf(segment, cursor);
|
|
14849
|
-
if (index === -1) {
|
|
14850
|
-
return false;
|
|
14883
|
+
* Simple extraction of `run:` values from YAML content.
|
|
14884
|
+
* Uses line-based parsing rather than a full YAML parser to avoid
|
|
14885
|
+
* adding yaml as a dependency to agent-shell.
|
|
14886
|
+
*/ function extractRunCommandsFromYaml(content) {
|
|
14887
|
+
const commands = [];
|
|
14888
|
+
const lines = content.split("\n");
|
|
14889
|
+
let inRunBlock = false;
|
|
14890
|
+
let runIndent = 0;
|
|
14891
|
+
let isFoldedBlock = false;
|
|
14892
|
+
let foldedLines = [];
|
|
14893
|
+
let continuationBuffer = "";
|
|
14894
|
+
function flushFoldedBlock() {
|
|
14895
|
+
if (foldedLines.length > 0) {
|
|
14896
|
+
commands.push(foldedLines.join(" "));
|
|
14897
|
+
foldedLines = [];
|
|
14851
14898
|
}
|
|
14852
|
-
cursor = index + segment.length;
|
|
14853
|
-
}
|
|
14854
|
-
const suffixStart = command.length - suffixSegment.length;
|
|
14855
|
-
return suffixStart >= cursor && command.endsWith(suffixSegment);
|
|
14856
|
-
}
|
|
14857
|
-
function evaluatePolicy(policy, command) {
|
|
14858
|
-
if (policy === null) {
|
|
14859
|
-
return {
|
|
14860
|
-
decision: "allow",
|
|
14861
|
-
matchedRule: null
|
|
14862
|
-
};
|
|
14863
14899
|
}
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
matchedRule: null
|
|
14869
|
-
};
|
|
14870
|
-
}
|
|
14871
|
-
for (const rule of policy.deny){
|
|
14872
|
-
if (matchesRule(trimmed, rule)) {
|
|
14873
|
-
return {
|
|
14874
|
-
decision: "deny",
|
|
14875
|
-
matchedRule: rule
|
|
14876
|
-
};
|
|
14900
|
+
function flushContinuation() {
|
|
14901
|
+
if (continuationBuffer.length > 0) {
|
|
14902
|
+
commands.push(continuationBuffer);
|
|
14903
|
+
continuationBuffer = "";
|
|
14877
14904
|
}
|
|
14878
14905
|
}
|
|
14879
|
-
|
|
14880
|
-
const
|
|
14881
|
-
if (
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
14906
|
+
for(let i = 0; i < lines.length; i++){
|
|
14907
|
+
const line = lines[i];
|
|
14908
|
+
if (line === undefined) continue;
|
|
14909
|
+
const trimmed = line.trimStart();
|
|
14910
|
+
const indent = line.length - trimmed.length;
|
|
14911
|
+
if (inRunBlock) {
|
|
14912
|
+
if (trimmed.length === 0) {
|
|
14913
|
+
continue;
|
|
14914
|
+
}
|
|
14915
|
+
if (indent > runIndent) {
|
|
14916
|
+
let cmd = trimmed.trimEnd();
|
|
14917
|
+
cmd = cmd.replace(/\s+#.*$/, "").trimEnd();
|
|
14918
|
+
const isContinuation = cmd.endsWith("\\");
|
|
14919
|
+
if (isContinuation) {
|
|
14920
|
+
cmd = cmd.slice(0, -1).trimEnd();
|
|
14921
|
+
}
|
|
14922
|
+
if (cmd.length > 0 && !cmd.startsWith("#")) {
|
|
14923
|
+
if (isFoldedBlock) {
|
|
14924
|
+
foldedLines.push(cmd);
|
|
14925
|
+
} else if (isContinuation) {
|
|
14926
|
+
continuationBuffer = continuationBuffer.length > 0 ? `${continuationBuffer} ${cmd}` : cmd;
|
|
14927
|
+
} else if (continuationBuffer.length > 0) {
|
|
14928
|
+
commands.push(`${continuationBuffer} ${cmd}`);
|
|
14929
|
+
continuationBuffer = "";
|
|
14930
|
+
} else {
|
|
14931
|
+
commands.push(cmd);
|
|
14932
|
+
}
|
|
14933
|
+
}
|
|
14934
|
+
} else {
|
|
14935
|
+
flushContinuation();
|
|
14936
|
+
if (isFoldedBlock) {
|
|
14937
|
+
flushFoldedBlock();
|
|
14938
|
+
}
|
|
14939
|
+
inRunBlock = false;
|
|
14940
|
+
isFoldedBlock = false;
|
|
14941
|
+
}
|
|
14942
|
+
}
|
|
14943
|
+
if (!inRunBlock) {
|
|
14944
|
+
const runMatch = trimmed.match(/^-?\s*run:\s*(.*)$/);
|
|
14945
|
+
if (runMatch) {
|
|
14946
|
+
const value = runMatch[1]?.trim();
|
|
14947
|
+
const firstToken = value ? value.split(/[ \t#]/, 1)[0] ?? "" : "";
|
|
14948
|
+
if (/^[|>][-+]?$/.test(firstToken)) {
|
|
14949
|
+
inRunBlock = true;
|
|
14950
|
+
runIndent = indent;
|
|
14951
|
+
isFoldedBlock = firstToken.startsWith(">");
|
|
14952
|
+
foldedLines = [];
|
|
14953
|
+
} else if (value && value.length > 0) {
|
|
14954
|
+
let command;
|
|
14955
|
+
const quoteMatch = value.match(/^(["'])(.*)\1/);
|
|
14956
|
+
if (quoteMatch) {
|
|
14957
|
+
command = quoteMatch[2] ?? "";
|
|
14958
|
+
} else {
|
|
14959
|
+
command = value.replace(/\s+#.*$/, "").trim();
|
|
14960
|
+
}
|
|
14961
|
+
if (command.length > 0 && !command.startsWith("#")) {
|
|
14962
|
+
commands.push(command);
|
|
14963
|
+
}
|
|
14964
|
+
}
|
|
14965
|
+
}
|
|
14886
14966
|
}
|
|
14887
14967
|
}
|
|
14888
|
-
|
|
14889
|
-
|
|
14890
|
-
|
|
14891
|
-
}
|
|
14968
|
+
flushContinuation();
|
|
14969
|
+
if (isFoldedBlock) {
|
|
14970
|
+
flushFoldedBlock();
|
|
14971
|
+
}
|
|
14972
|
+
return commands;
|
|
14892
14973
|
}
|
|
14893
14974
|
/**
|
|
14894
|
-
*
|
|
14895
|
-
*
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
return `\\x${ch.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
|
14901
|
-
});
|
|
14902
|
-
}
|
|
14903
|
-
function resolvePolicyPath(env, repoRoot) {
|
|
14904
|
-
const override = env.AGENTSHELL_POLICY_PATH;
|
|
14905
|
-
if (override !== undefined && override !== "") {
|
|
14906
|
-
if ((0,external_node_path_namespaceObject.isAbsolute)(override)) {
|
|
14907
|
-
// Absolute path — use as-is (will be validated after realpath)
|
|
14908
|
-
return {
|
|
14909
|
-
path: override,
|
|
14910
|
-
isOverride: true
|
|
14911
|
-
};
|
|
14912
|
-
}
|
|
14913
|
-
// Relative path — resolve relative to repo root
|
|
14914
|
-
return {
|
|
14915
|
-
path: (0,external_node_path_namespaceObject.join)(repoRoot, override),
|
|
14916
|
-
isOverride: true
|
|
14917
|
-
};
|
|
14975
|
+
* Parse mise.toml to extract task definitions.
|
|
14976
|
+
* Uses simple line-based parsing for [tasks.*] sections.
|
|
14977
|
+
*/ async function discoverMiseTasks(targetDir) {
|
|
14978
|
+
const miseTomlPath = (0,external_node_path_namespaceObject.join)(targetDir, "mise.toml");
|
|
14979
|
+
if (!await fileExists(miseTomlPath)) {
|
|
14980
|
+
return [];
|
|
14918
14981
|
}
|
|
14919
|
-
return {
|
|
14920
|
-
path: (0,external_node_path_namespaceObject.join)(repoRoot, DEFAULT_POLICY_SUBPATH),
|
|
14921
|
-
isOverride: false
|
|
14922
|
-
};
|
|
14923
|
-
}
|
|
14924
|
-
async function loadPolicy(env, deps) {
|
|
14925
|
-
const rawRepoRoot = deps.getRepositoryRoot();
|
|
14926
|
-
const repoRoot = await deps.realpath(rawRepoRoot);
|
|
14927
|
-
const { path: candidatePath, isOverride } = resolvePolicyPath(env, repoRoot);
|
|
14928
|
-
let resolvedPath;
|
|
14929
14982
|
try {
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
if (isOverride) {
|
|
14934
|
-
throw new Error(`Policy override path does not exist: ${sanitizePath(candidatePath)}`);
|
|
14935
|
-
}
|
|
14936
|
-
return null;
|
|
14983
|
+
const fileStat = await (0,promises_namespaceObject.stat)(miseTomlPath);
|
|
14984
|
+
if (fileStat.size > MAX_FILE_SIZE_BYTES) {
|
|
14985
|
+
return [];
|
|
14937
14986
|
}
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14987
|
+
const content = await (0,promises_namespaceObject.readFile)(miseTomlPath, "utf-8");
|
|
14988
|
+
return parseMiseTomlTasks(content);
|
|
14989
|
+
} catch {
|
|
14990
|
+
return [];
|
|
14942
14991
|
}
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14992
|
+
}
|
|
14993
|
+
function parseMiseTomlTasks(content) {
|
|
14994
|
+
const tasks = [];
|
|
14995
|
+
const lines = content.split("\n");
|
|
14996
|
+
let currentTaskName = null;
|
|
14997
|
+
let inMultiLineRun = false;
|
|
14998
|
+
let multiLineCommand = "";
|
|
14999
|
+
for (const line of lines){
|
|
15000
|
+
const trimmed = line.trim();
|
|
15001
|
+
if (inMultiLineRun) {
|
|
15002
|
+
if (trimmed === '"""' || trimmed === "'''") {
|
|
15003
|
+
inMultiLineRun = false;
|
|
15004
|
+
const cmd = multiLineCommand.trim();
|
|
15005
|
+
if (currentTaskName !== null && cmd.length > 0) {
|
|
15006
|
+
const firstLine = cmd.split("\n").map((l)=>l.trim()).find((l)=>l.length > 0);
|
|
15007
|
+
if (firstLine) {
|
|
15008
|
+
tasks.push({
|
|
15009
|
+
name: currentTaskName,
|
|
15010
|
+
command: firstLine
|
|
15011
|
+
});
|
|
15012
|
+
}
|
|
15013
|
+
}
|
|
15014
|
+
multiLineCommand = "";
|
|
15015
|
+
continue;
|
|
15016
|
+
}
|
|
15017
|
+
multiLineCommand += `${trimmed}\n`;
|
|
15018
|
+
continue;
|
|
15019
|
+
}
|
|
15020
|
+
const sectionMatch = trimmed.match(/^\[tasks\.([^\]]+)\]$/);
|
|
15021
|
+
if (sectionMatch?.[1]) {
|
|
15022
|
+
currentTaskName = sectionMatch[1];
|
|
15023
|
+
continue;
|
|
15024
|
+
}
|
|
15025
|
+
if (trimmed.startsWith("[") && !trimmed.startsWith("[tasks.")) {
|
|
15026
|
+
currentTaskName = null;
|
|
15027
|
+
continue;
|
|
15028
|
+
}
|
|
15029
|
+
if (currentTaskName !== null) {
|
|
15030
|
+
if (/^run\s*=\s*"""/.test(trimmed) || /^run\s*=\s*'''/.test(trimmed)) {
|
|
15031
|
+
inMultiLineRun = true;
|
|
15032
|
+
multiLineCommand = "";
|
|
15033
|
+
continue;
|
|
15034
|
+
}
|
|
15035
|
+
const runMatch = trimmed.match(/^run\s*=\s*(?:"([^"]*)"|'([^']*)')/);
|
|
15036
|
+
if (runMatch) {
|
|
15037
|
+
const command = runMatch[1] ?? runMatch[2] ?? "";
|
|
15038
|
+
if (command.length > 0) {
|
|
15039
|
+
tasks.push({
|
|
15040
|
+
name: currentTaskName,
|
|
15041
|
+
command
|
|
15042
|
+
});
|
|
15043
|
+
}
|
|
14950
15044
|
}
|
|
14951
|
-
return null;
|
|
14952
15045
|
}
|
|
14953
|
-
throw error;
|
|
14954
|
-
}
|
|
14955
|
-
let parsed;
|
|
14956
|
-
try {
|
|
14957
|
-
parsed = JSON.parse(content);
|
|
14958
|
-
} catch {
|
|
14959
|
-
throw new Error(`Invalid JSON in policy file ${sanitizePath(resolvedPath)}: file exists but contains malformed JSON`);
|
|
14960
|
-
}
|
|
14961
|
-
return PolicyConfigSchema.parse(parsed);
|
|
14962
|
-
}
|
|
14963
|
-
|
|
14964
|
-
;// CONCATENATED MODULE: ./src/actor.ts
|
|
14965
|
-
// Best-effort agent detection env vars (Phase 1):
|
|
14966
|
-
// - CLAUDE_CODE: Set by Claude Code (`CLAUDE_CODE=1`) in its shell sessions
|
|
14967
|
-
// - COPILOT_AGENT: Set by GitHub Copilot coding agent in its shell sessions
|
|
14968
|
-
// - COPILOT_CLI: Set by GitHub Copilot CLI (`COPILOT_CLI=1`) in its shell sessions
|
|
14969
|
-
// - COPILOT_CLI_BINARY_VERSION: Set by GitHub Copilot CLI (e.g. `COPILOT_CLI_BINARY_VERSION=1.0.4`)
|
|
14970
|
-
// If these prove incorrect, the detection rule should be removed entirely.
|
|
14971
|
-
/**
|
|
14972
|
-
* Determines who initiated a script execution based on environment variables.
|
|
14973
|
-
*
|
|
14974
|
-
* Detection priority (first match wins):
|
|
14975
|
-
* 1. Explicit override via AGENTSHELL_ACTOR
|
|
14976
|
-
* 2. CI detection via GITHUB_ACTIONS
|
|
14977
|
-
* 3. Known coding agent detection (Claude Code, GitHub Copilot)
|
|
14978
|
-
* 4. Fallback to "human"
|
|
14979
|
-
*/ function detectActor(env) {
|
|
14980
|
-
const override = env.AGENTSHELL_ACTOR;
|
|
14981
|
-
if (override !== undefined && override !== "") {
|
|
14982
|
-
return override;
|
|
14983
|
-
}
|
|
14984
|
-
if (env.GITHUB_ACTIONS === "true") {
|
|
14985
|
-
return "ci";
|
|
14986
|
-
}
|
|
14987
|
-
if (env.CLAUDE_CODE !== undefined && env.CLAUDE_CODE !== "") {
|
|
14988
|
-
return "claude-code";
|
|
14989
|
-
}
|
|
14990
|
-
if (env.COPILOT_AGENT !== undefined && env.COPILOT_AGENT !== "") {
|
|
14991
|
-
return "copilot";
|
|
14992
|
-
}
|
|
14993
|
-
if (env.COPILOT_CLI !== undefined && env.COPILOT_CLI !== "" || env.COPILOT_CLI_BINARY_VERSION !== undefined && env.COPILOT_CLI_BINARY_VERSION !== "") {
|
|
14994
|
-
return "copilot";
|
|
14995
15046
|
}
|
|
14996
|
-
return
|
|
15047
|
+
return tasks;
|
|
14997
15048
|
}
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
const
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
const PROTOTYPE_POLLUTION_KEYS = new Set([
|
|
15005
|
-
"__proto__",
|
|
15006
|
-
"constructor",
|
|
15007
|
-
"prototype"
|
|
15008
|
-
]);
|
|
15009
|
-
const ALLOWLIST_PREFIXES = [
|
|
15010
|
-
"npm_lifecycle_",
|
|
15011
|
-
"github_",
|
|
15012
|
-
"agentshell_"
|
|
15013
|
-
];
|
|
15014
|
-
const ALLOWLIST_EXACT = new Set([
|
|
15015
|
-
"npm_package_name",
|
|
15016
|
-
"npm_package_version",
|
|
15017
|
-
"node_env",
|
|
15018
|
-
"ci"
|
|
15019
|
-
]);
|
|
15020
|
-
const BLOCKLIST_PATTERNS = [
|
|
15021
|
-
"secret",
|
|
15022
|
-
"token",
|
|
15023
|
-
"key",
|
|
15024
|
-
"password",
|
|
15025
|
-
"credential",
|
|
15026
|
-
"auth"
|
|
15027
|
-
];
|
|
15028
|
-
function isAllowlisted(name) {
|
|
15029
|
-
const lower = name.toLowerCase();
|
|
15030
|
-
if (ALLOWLIST_EXACT.has(lower)) {
|
|
15031
|
-
return true;
|
|
15049
|
+
async function detectLanguages(targetDir) {
|
|
15050
|
+
const detected = new Set();
|
|
15051
|
+
for (const [filename, language] of Object.entries(LANGUAGE_MARKERS)){
|
|
15052
|
+
if (await fileExists((0,external_node_path_namespaceObject.join)(targetDir, filename))) {
|
|
15053
|
+
detected.add(language);
|
|
15054
|
+
}
|
|
15032
15055
|
}
|
|
15033
|
-
return
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
const lower = name.toLowerCase();
|
|
15037
|
-
return BLOCKLIST_PATTERNS.some((pattern)=>lower.includes(pattern));
|
|
15038
|
-
}
|
|
15039
|
-
function isTagVariable(name) {
|
|
15040
|
-
return name.startsWith(TAG_PREFIX);
|
|
15056
|
+
return [
|
|
15057
|
+
...detected
|
|
15058
|
+
];
|
|
15041
15059
|
}
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15060
|
+
/**
|
|
15061
|
+
* Scans a project directory to discover tools, commands, and languages.
|
|
15062
|
+
* Uses static file analysis without requiring external dependencies.
|
|
15063
|
+
*/ async function scanProject(targetDir) {
|
|
15064
|
+
const [scripts, workflowCommands, miseTasks, languages] = await Promise.all([
|
|
15065
|
+
discoverScripts(targetDir),
|
|
15066
|
+
discoverWorkflowCommands(targetDir),
|
|
15067
|
+
discoverMiseTasks(targetDir),
|
|
15068
|
+
detectLanguages(targetDir)
|
|
15069
|
+
]);
|
|
15052
15070
|
return {
|
|
15053
|
-
|
|
15054
|
-
|
|
15071
|
+
scripts,
|
|
15072
|
+
workflowCommands,
|
|
15073
|
+
miseTasks,
|
|
15074
|
+
languages
|
|
15055
15075
|
};
|
|
15056
15076
|
}
|
|
15057
|
-
function captureEnv(env) {
|
|
15058
|
-
const result = Object.create(null);
|
|
15059
|
-
let anyTruncated = false;
|
|
15060
|
-
for (const [name, value] of Object.entries(env)){
|
|
15061
|
-
if (value === undefined) continue;
|
|
15062
|
-
if (!isAllowlisted(name)) continue;
|
|
15063
|
-
if (isBlocklisted(name)) continue;
|
|
15064
|
-
if (isTagVariable(name)) continue;
|
|
15065
|
-
const { value: finalValue, truncated } = truncateValue(value);
|
|
15066
|
-
result[name] = finalValue;
|
|
15067
|
-
if (truncated) anyTruncated = true;
|
|
15068
|
-
}
|
|
15069
|
-
if (anyTruncated) {
|
|
15070
|
-
result._env_truncated = "true";
|
|
15071
|
-
}
|
|
15072
|
-
return result;
|
|
15073
|
-
}
|
|
15074
|
-
function captureTags(env) {
|
|
15075
|
-
const result = Object.create(null);
|
|
15076
|
-
let anyTruncatedOrDiscarded = false;
|
|
15077
|
-
const entries = [];
|
|
15078
|
-
for (const [name, value] of Object.entries(env)){
|
|
15079
|
-
if (value === undefined) continue;
|
|
15080
|
-
if (!name.startsWith(TAG_PREFIX)) continue;
|
|
15081
|
-
const tagKey = name.slice(TAG_PREFIX.length).toLowerCase();
|
|
15082
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(tagKey)) continue;
|
|
15083
|
-
entries.push([
|
|
15084
|
-
tagKey,
|
|
15085
|
-
value
|
|
15086
|
-
]);
|
|
15087
|
-
}
|
|
15088
|
-
entries.sort((a, b)=>a[0].localeCompare(b[0]));
|
|
15089
|
-
if (entries.length > MAX_TAGS) {
|
|
15090
|
-
anyTruncatedOrDiscarded = true;
|
|
15091
|
-
entries.length = MAX_TAGS;
|
|
15092
|
-
}
|
|
15093
|
-
for (const [key, rawValue] of entries){
|
|
15094
|
-
const { value, truncated } = truncateValue(rawValue);
|
|
15095
|
-
result[key] = value;
|
|
15096
|
-
if (truncated) anyTruncatedOrDiscarded = true;
|
|
15097
|
-
}
|
|
15098
|
-
if (anyTruncatedOrDiscarded) {
|
|
15099
|
-
result._tags_truncated = "true";
|
|
15100
|
-
}
|
|
15101
|
-
return result;
|
|
15102
|
-
}
|
|
15103
15077
|
|
|
15104
|
-
;// CONCATENATED MODULE: ./src/
|
|
15105
|
-
// biome-ignore-all lint/style/useNamingConvention: telemetry schema uses snake_case field names
|
|
15078
|
+
;// CONCATENATED MODULE: ./src/policy-init.ts
|
|
15106
15079
|
|
|
15107
15080
|
|
|
15108
15081
|
|
|
15109
15082
|
|
|
15110
15083
|
|
|
15111
|
-
|
|
15112
|
-
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15130
|
-
|
|
15131
|
-
|
|
15132
|
-
|
|
15133
|
-
|
|
15134
|
-
|
|
15135
|
-
|
|
15136
|
-
|
|
15137
|
-
|
|
15138
|
-
|
|
15139
|
-
const
|
|
15140
|
-
const
|
|
15141
|
-
|
|
15142
|
-
|
|
15143
|
-
const projectRootReal = await deps.realpath(projectRoot);
|
|
15144
|
-
// Reject external paths (allowing either logical or real project root)
|
|
15145
|
-
const resolvedLogical = (0,external_node_path_namespaceObject.resolve)(projectRoot, logDir);
|
|
15146
|
-
if (!isWithinProjectRoot(resolvedLogical, projectRoot) && !isWithinProjectRoot(resolvedLogical, projectRootReal)) {
|
|
15147
|
-
deps.writeStderr(`agent-shell: AGENTSHELL_LOG_DIR resolves outside project root, using default\n`);
|
|
15148
|
-
await deps.mkdir(defaultDir, {
|
|
15149
|
-
recursive: true
|
|
15150
|
-
});
|
|
15151
|
-
return defaultDir;
|
|
15084
|
+
// Exact-match entries for agent-shell's own commands prevent the preToolUse
|
|
15085
|
+
// hook from blocking its sibling hooks. Wildcard entries (ending with *) cover
|
|
15086
|
+
// common read-only git commands. The final allow list is sorted alphabetically.
|
|
15087
|
+
const DEFAULT_SAFE_COMMANDS = [
|
|
15088
|
+
"agent-shell policy-check",
|
|
15089
|
+
"agent-shell record",
|
|
15090
|
+
"git status *",
|
|
15091
|
+
"git diff *",
|
|
15092
|
+
"git log *",
|
|
15093
|
+
"git show *",
|
|
15094
|
+
"git branch --show-current",
|
|
15095
|
+
"git branch --list *",
|
|
15096
|
+
"git rev-parse *",
|
|
15097
|
+
"pwd"
|
|
15098
|
+
];
|
|
15099
|
+
const DEFAULT_DENY_RULES = [
|
|
15100
|
+
"rm -rf *",
|
|
15101
|
+
"sudo *"
|
|
15102
|
+
];
|
|
15103
|
+
const POLICY_SUBPATH = ".github/hooks/agent-shell/policy.json";
|
|
15104
|
+
const HOOKS_SUBPATH = ".github/hooks/agent-shell/hooks.json";
|
|
15105
|
+
/**
|
|
15106
|
+
* Extracts the script or task name from a command string, skipping any
|
|
15107
|
+
* flags (tokens starting with `-`) that appear between the prefix and
|
|
15108
|
+
* the actual name. For example, `npm run -s build` or `npm run --silent build`
|
|
15109
|
+
* both return `build`. Splits on any whitespace to avoid empty tokens from
|
|
15110
|
+
* multiple consecutive spaces.
|
|
15111
|
+
*/ function extractNameAfterPrefix(cmd, prefix) {
|
|
15112
|
+
const tokens = cmd.slice(prefix.length).trim().split(/\s+/);
|
|
15113
|
+
for (const token of tokens){
|
|
15114
|
+
if (token.length > 0 && !token.startsWith("-")) {
|
|
15115
|
+
return token;
|
|
15152
15116
|
}
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15158
|
-
|
|
15159
|
-
|
|
15160
|
-
|
|
15117
|
+
}
|
|
15118
|
+
return "";
|
|
15119
|
+
}
|
|
15120
|
+
/**
|
|
15121
|
+
* Generates a policy configuration from project scan results.
|
|
15122
|
+
* Creates an allow list of commands discovered in the project,
|
|
15123
|
+
* plus common safe defaults. Includes standard deny rules.
|
|
15124
|
+
*
|
|
15125
|
+
* Allow rules use exact match by default to prevent shell
|
|
15126
|
+
* metacharacter bypass (e.g. `npm test && curl evil`).
|
|
15127
|
+
* Wildcard `*` is only used for commands where subcommand
|
|
15128
|
+
* arguments are inherently expected and the base command
|
|
15129
|
+
* is genuinely read-only (e.g. `git status *`).
|
|
15130
|
+
*/ function generatePolicy(scanResult) {
|
|
15131
|
+
const allowSet = new Set();
|
|
15132
|
+
for (const cmd of DEFAULT_SAFE_COMMANDS){
|
|
15133
|
+
allowSet.add(cmd);
|
|
15134
|
+
}
|
|
15135
|
+
for (const script of scanResult.scripts){
|
|
15136
|
+
const name = script.name.trim();
|
|
15137
|
+
if (name.length === 0) {
|
|
15138
|
+
continue;
|
|
15161
15139
|
}
|
|
15162
|
-
|
|
15163
|
-
|
|
15164
|
-
}
|
|
15165
|
-
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
recursive: true
|
|
15170
|
-
});
|
|
15171
|
-
return defaultDir;
|
|
15140
|
+
if (SHELL_METACHAR_PATTERN.test(name)) {
|
|
15141
|
+
continue;
|
|
15142
|
+
}
|
|
15143
|
+
if (name === "test") {
|
|
15144
|
+
allowSet.add("npm test");
|
|
15145
|
+
} else {
|
|
15146
|
+
allowSet.add(`npm run ${name}`);
|
|
15172
15147
|
}
|
|
15173
|
-
return resolved;
|
|
15174
15148
|
}
|
|
15175
|
-
|
|
15149
|
+
for (const cmd of scanResult.workflowCommands){
|
|
15150
|
+
if (cmd === "npm test" || cmd.startsWith("npm test ")) {
|
|
15151
|
+
allowSet.add("npm test");
|
|
15152
|
+
if (cmd !== "npm test" && !SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
15153
|
+
allowSet.add(cmd);
|
|
15154
|
+
}
|
|
15155
|
+
} else if (cmd.startsWith("npm run ")) {
|
|
15156
|
+
const scriptName = extractNameAfterPrefix(cmd, "npm run ");
|
|
15157
|
+
if (scriptName) {
|
|
15158
|
+
allowSet.add(`npm run ${scriptName}`);
|
|
15159
|
+
if (cmd !== `npm run ${scriptName}` && !SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
15160
|
+
allowSet.add(cmd);
|
|
15161
|
+
}
|
|
15162
|
+
} else if (!SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
15163
|
+
allowSet.add(cmd);
|
|
15164
|
+
}
|
|
15165
|
+
} else if (cmd === "npm ci" || cmd === "npm install") {
|
|
15166
|
+
allowSet.add(cmd);
|
|
15167
|
+
} else if (cmd.startsWith("npx ")) {
|
|
15168
|
+
if (!SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
15169
|
+
allowSet.add(cmd);
|
|
15170
|
+
}
|
|
15171
|
+
} else if (cmd.startsWith("mise run ")) {
|
|
15172
|
+
const taskName = extractNameAfterPrefix(cmd, "mise run ");
|
|
15173
|
+
if (taskName) {
|
|
15174
|
+
allowSet.add(`mise run ${taskName}`);
|
|
15175
|
+
if (cmd !== `mise run ${taskName}` && !SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
15176
|
+
allowSet.add(cmd);
|
|
15177
|
+
}
|
|
15178
|
+
} else if (!SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
15179
|
+
allowSet.add(cmd);
|
|
15180
|
+
}
|
|
15181
|
+
} else {
|
|
15182
|
+
if (!SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
15183
|
+
allowSet.add(cmd);
|
|
15184
|
+
}
|
|
15185
|
+
}
|
|
15186
|
+
}
|
|
15187
|
+
for (const task of scanResult.miseTasks){
|
|
15188
|
+
const taskName = task.name.trim();
|
|
15189
|
+
if (taskName.length > 0 && !SHELL_METACHAR_PATTERN.test(taskName)) {
|
|
15190
|
+
allowSet.add(`mise run ${taskName}`);
|
|
15191
|
+
}
|
|
15192
|
+
}
|
|
15193
|
+
if (scanResult.miseTasks.length > 0) {
|
|
15194
|
+
allowSet.add("mise install");
|
|
15195
|
+
}
|
|
15196
|
+
const allow = [
|
|
15197
|
+
...allowSet
|
|
15198
|
+
].sort();
|
|
15199
|
+
return {
|
|
15200
|
+
allow,
|
|
15201
|
+
deny: [
|
|
15202
|
+
...DEFAULT_DENY_RULES
|
|
15203
|
+
]
|
|
15204
|
+
};
|
|
15205
|
+
}
|
|
15206
|
+
/**
|
|
15207
|
+
* Generates the Copilot hooks.json configuration with agent-shell
|
|
15208
|
+
* hooks based on the provided feature flags.
|
|
15209
|
+
* Defaults to policyCheck only for backward compatibility.
|
|
15210
|
+
*/ function generateHooksConfig(options = {}) {
|
|
15211
|
+
const { flightRecorder, policyCheck } = {
|
|
15212
|
+
policyCheck: true,
|
|
15213
|
+
...options
|
|
15214
|
+
};
|
|
15215
|
+
const config = {
|
|
15216
|
+
version: 1,
|
|
15217
|
+
hooks: {}
|
|
15218
|
+
};
|
|
15219
|
+
if (policyCheck) {
|
|
15220
|
+
config.hooks.preToolUse = [
|
|
15221
|
+
{
|
|
15222
|
+
type: "command",
|
|
15223
|
+
bash: "agent-shell policy-check",
|
|
15224
|
+
timeoutSec: 30
|
|
15225
|
+
}
|
|
15226
|
+
];
|
|
15227
|
+
}
|
|
15228
|
+
if (flightRecorder) {
|
|
15229
|
+
config.hooks.postToolUse = [
|
|
15230
|
+
{
|
|
15231
|
+
type: "command",
|
|
15232
|
+
bash: "agent-shell record",
|
|
15233
|
+
timeoutSec: 30
|
|
15234
|
+
}
|
|
15235
|
+
];
|
|
15236
|
+
}
|
|
15237
|
+
return config;
|
|
15238
|
+
}
|
|
15239
|
+
/**
|
|
15240
|
+
* Writes a file atomically, skipping if the file already exists.
|
|
15241
|
+
* Uses the `wx` (exclusive create) flag to avoid TOCTOU races
|
|
15242
|
+
* between a check and write.
|
|
15243
|
+
*/ async function writeFileIfNotExists(filePath, parentDir, content, subpath, writeStdout) {
|
|
15244
|
+
await (0,promises_namespaceObject.mkdir)(parentDir, {
|
|
15176
15245
|
recursive: true
|
|
15177
15246
|
});
|
|
15178
|
-
|
|
15247
|
+
try {
|
|
15248
|
+
await (0,promises_namespaceObject.writeFile)(filePath, content, {
|
|
15249
|
+
flag: "wx"
|
|
15250
|
+
});
|
|
15251
|
+
writeStdout(`Created ${subpath}\n`);
|
|
15252
|
+
} catch (error) {
|
|
15253
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
15254
|
+
writeStdout(`Skipping ${subpath} — file already exists\n`);
|
|
15255
|
+
} else {
|
|
15256
|
+
throw error;
|
|
15257
|
+
}
|
|
15258
|
+
}
|
|
15179
15259
|
}
|
|
15180
|
-
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
|
|
15260
|
+
/**
|
|
15261
|
+
* Handles the `policy --init` command.
|
|
15262
|
+
* Scans the project, generates a policy and hooks configuration,
|
|
15263
|
+
* and writes them to disk. Optionally uses @github/copilot-sdk
|
|
15264
|
+
* for AI-enhanced analysis when available.
|
|
15265
|
+
*/ async function handlePolicyInit(deps) {
|
|
15266
|
+
const repoRoot = deps.getRepositoryRoot();
|
|
15267
|
+
deps.writeStdout("Scanning project...\n");
|
|
15268
|
+
const scanResult = await scanProject(repoRoot);
|
|
15269
|
+
deps.writeStdout(`Discovered: ${scanResult.scripts.length} npm script(s), ` + `${scanResult.workflowCommands.length} workflow command(s), ` + `${scanResult.miseTasks.length} mise task(s), ` + `${scanResult.languages.length} language(s)\n`);
|
|
15270
|
+
const policy = generatePolicy(scanResult);
|
|
15271
|
+
const enhanced = await enhanceWithCopilot(scanResult, repoRoot, deps.writeStderr, deps.model);
|
|
15272
|
+
if (enhanced !== null) {
|
|
15273
|
+
deps.writeStdout("Enhanced with Copilot analysis\n");
|
|
15274
|
+
if (enhanced.additionalAllowRules.length > 0) {
|
|
15275
|
+
deps.writeStdout("\nSuggested additional allow rules from Copilot (not auto-applied):\n");
|
|
15276
|
+
for (const rule of enhanced.additionalAllowRules){
|
|
15277
|
+
if (isSafeCommand(rule)) {
|
|
15278
|
+
deps.writeStdout(` - ${sanitizeOutput(rule)}\n`);
|
|
15279
|
+
} else {
|
|
15280
|
+
deps.writeStdout(` - [UNSAFE, skipped] ${sanitizeOutput(rule)}\n`);
|
|
15281
|
+
}
|
|
15282
|
+
}
|
|
15283
|
+
}
|
|
15284
|
+
if (enhanced.suggestions.length > 0) {
|
|
15285
|
+
deps.writeStdout("\nSuggestions from Copilot:\n");
|
|
15286
|
+
for (const suggestion of enhanced.suggestions){
|
|
15287
|
+
deps.writeStdout(` - ${sanitizeOutput(suggestion)}\n`);
|
|
15288
|
+
}
|
|
15289
|
+
}
|
|
15290
|
+
}
|
|
15291
|
+
const hooksConfig = generateHooksConfig();
|
|
15292
|
+
const policyPath = (0,external_node_path_namespaceObject.join)(repoRoot, POLICY_SUBPATH);
|
|
15293
|
+
const hooksPath = (0,external_node_path_namespaceObject.join)(repoRoot, HOOKS_SUBPATH);
|
|
15294
|
+
const policyContent = `${JSON.stringify(policy, null, 2)}\n`;
|
|
15295
|
+
const hooksContent = `${JSON.stringify(hooksConfig, null, 2)}\n`;
|
|
15296
|
+
await writeFileIfNotExists(policyPath, (0,external_node_path_namespaceObject.join)(repoRoot, ".github", "hooks", "agent-shell"), policyContent, POLICY_SUBPATH, deps.writeStdout);
|
|
15297
|
+
await writeFileIfNotExists(hooksPath, (0,external_node_path_namespaceObject.join)(repoRoot, ".github", "hooks", "agent-shell"), hooksContent, HOOKS_SUBPATH, deps.writeStdout);
|
|
15298
|
+
deps.writeStdout("\n--- Proposed Policy ---\n");
|
|
15299
|
+
deps.writeStdout(`${sanitizeOutput(JSON.stringify(policy, null, 2))}\n`);
|
|
15300
|
+
deps.writeStdout("\n--- Hook Configuration ---\n");
|
|
15301
|
+
deps.writeStdout(`${sanitizeOutput(JSON.stringify(hooksConfig, null, 2))}\n`);
|
|
15184
15302
|
}
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
|
|
15188
|
-
|
|
15189
|
-
|
|
15190
|
-
|
|
15191
|
-
|
|
15192
|
-
|
|
15193
|
-
|
|
15194
|
-
|
|
15195
|
-
|
|
15196
|
-
|
|
15197
|
-
|
|
15198
|
-
|
|
15199
|
-
|
|
15200
|
-
|
|
15201
|
-
|
|
15202
|
-
|
|
15203
|
-
|
|
15204
|
-
|
|
15205
|
-
|
|
15206
|
-
|
|
15207
|
-
|
|
15208
|
-
|
|
15209
|
-
|
|
15303
|
+
|
|
15304
|
+
;// CONCATENATED MODULE: ./src/types.ts
|
|
15305
|
+
// biome-ignore-all lint/style/useNamingConvention: telemetry schema uses snake_case field names
|
|
15306
|
+
|
|
15307
|
+
const SCHEMA_VERSION = 1;
|
|
15308
|
+
const baseFields = {
|
|
15309
|
+
v: literal(1),
|
|
15310
|
+
session_id: schemas_string(),
|
|
15311
|
+
command: schemas_string(),
|
|
15312
|
+
actor: schemas_string(),
|
|
15313
|
+
timestamp: schemas_string(),
|
|
15314
|
+
env: record(schemas_string(), schemas_string()),
|
|
15315
|
+
tags: record(schemas_string(), schemas_string())
|
|
15316
|
+
};
|
|
15317
|
+
const ScriptEndEventSchema = schemas_object({
|
|
15318
|
+
...baseFields,
|
|
15319
|
+
event: literal("script_end"),
|
|
15320
|
+
script: schemas_string().optional(),
|
|
15321
|
+
package: schemas_string().optional(),
|
|
15322
|
+
package_version: schemas_string().optional(),
|
|
15323
|
+
exit_code: schemas_number().int(),
|
|
15324
|
+
signal: schemas_string().nullable(),
|
|
15325
|
+
duration_ms: schemas_number()
|
|
15326
|
+
}).strict();
|
|
15327
|
+
const ShimErrorEventSchema = schemas_object({
|
|
15328
|
+
...baseFields,
|
|
15329
|
+
event: literal("shim_error")
|
|
15330
|
+
}).strict();
|
|
15331
|
+
const PolicyDecisionEventSchema = schemas_object({
|
|
15332
|
+
...baseFields,
|
|
15333
|
+
event: literal("policy_decision"),
|
|
15334
|
+
decision: schemas_enum([
|
|
15335
|
+
"allow",
|
|
15336
|
+
"deny"
|
|
15337
|
+
]),
|
|
15338
|
+
matched_rule: schemas_string().nullable()
|
|
15339
|
+
}).strict();
|
|
15340
|
+
const ToolUseEventSchema = schemas_object({
|
|
15341
|
+
...baseFields,
|
|
15342
|
+
event: literal("tool_use"),
|
|
15343
|
+
tool_name: schemas_string()
|
|
15344
|
+
}).strict();
|
|
15345
|
+
const ScriptEventSchema = discriminatedUnion("event", [
|
|
15346
|
+
ScriptEndEventSchema,
|
|
15347
|
+
ShimErrorEventSchema,
|
|
15348
|
+
PolicyDecisionEventSchema,
|
|
15349
|
+
ToolUseEventSchema
|
|
15350
|
+
]);
|
|
15351
|
+
const MAX_POLICY_RULES = 10_000;
|
|
15352
|
+
const MAX_RULE_LENGTH = 1024;
|
|
15353
|
+
const policyRuleArray = schemas_array(schemas_string().max(MAX_RULE_LENGTH)).max(MAX_POLICY_RULES);
|
|
15354
|
+
const PolicyConfigSchema = schemas_object({
|
|
15355
|
+
allow: policyRuleArray.optional(),
|
|
15356
|
+
deny: policyRuleArray.default(()=>[])
|
|
15357
|
+
}).strict();
|
|
15358
|
+
// NOTE: This schema mirrors CopilotHookCommandSchema in @lousy-agents/core
|
|
15359
|
+
// (packages/core/src/entities/copilot-hook-schema.ts). Keep them aligned.
|
|
15360
|
+
// agent-shell cannot import from core since it is a standalone published binary.
|
|
15361
|
+
const MAX_HOOKS_PER_EVENT = 100;
|
|
15362
|
+
/** Regex that allows standard env var names and rejects __proto__ (the prototype-polluting key). */ const ENV_KEY_PATTERN = /^(?!__proto__$)[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
15363
|
+
const HookCommandSchema = schemas_object({
|
|
15364
|
+
type: literal("command"),
|
|
15365
|
+
bash: schemas_string().min(1, "Hook bash command must not be empty").optional(),
|
|
15366
|
+
powershell: schemas_string().min(1, "Hook PowerShell command must not be empty").optional(),
|
|
15367
|
+
cwd: schemas_string().optional(),
|
|
15368
|
+
timeoutSec: schemas_number().positive().optional(),
|
|
15369
|
+
env: record(schemas_string().regex(ENV_KEY_PATTERN, "Hook env key must be a valid identifier (no prototype-polluting keys)"), schemas_string()).optional()
|
|
15370
|
+
}).strict().refine((data)=>Boolean(data.bash) || Boolean(data.powershell), {
|
|
15371
|
+
message: "At least one of 'bash' or 'powershell' must be provided and non-empty"
|
|
15372
|
+
});
|
|
15373
|
+
const hookArray = schemas_array(HookCommandSchema).max(MAX_HOOKS_PER_EVENT);
|
|
15374
|
+
const HooksConfigSchema = schemas_object({
|
|
15375
|
+
version: literal(1),
|
|
15376
|
+
hooks: schemas_object({
|
|
15377
|
+
sessionStart: hookArray.optional(),
|
|
15378
|
+
userPromptSubmitted: hookArray.optional(),
|
|
15379
|
+
preToolUse: hookArray.optional(),
|
|
15380
|
+
postToolUse: hookArray.optional(),
|
|
15381
|
+
sessionEnd: hookArray.optional()
|
|
15382
|
+
}).strict()
|
|
15383
|
+
}).strict();
|
|
15384
|
+
|
|
15385
|
+
;// CONCATENATED MODULE: ./src/init-command.ts
|
|
15386
|
+
|
|
15387
|
+
|
|
15388
|
+
|
|
15389
|
+
|
|
15390
|
+
|
|
15391
|
+
|
|
15392
|
+
const init_command_HOOKS_SUBPATH = ".github/hooks/agent-shell/hooks.json";
|
|
15393
|
+
const init_command_POLICY_SUBPATH = ".github/hooks/agent-shell/policy.json";
|
|
15394
|
+
const HOOKS_PARENT = ".github/hooks/agent-shell";
|
|
15395
|
+
const AGENT_SHELL_POLICY_CHECK = "agent-shell policy-check";
|
|
15396
|
+
const AGENT_SHELL_RECORD = "agent-shell record";
|
|
15397
|
+
const AGENT_SHELL_ALLOW_ENTRIES = [
|
|
15398
|
+
AGENT_SHELL_POLICY_CHECK,
|
|
15399
|
+
AGENT_SHELL_RECORD
|
|
15400
|
+
];
|
|
15401
|
+
function hasHookCommand(hooks, expectedCommand) {
|
|
15402
|
+
if (!Array.isArray(hooks)) {
|
|
15403
|
+
return false;
|
|
15404
|
+
}
|
|
15405
|
+
return hooks.some((hook)=>{
|
|
15406
|
+
if (typeof hook !== "object" || hook === null) {
|
|
15407
|
+
return false;
|
|
15210
15408
|
}
|
|
15211
|
-
|
|
15212
|
-
|
|
15409
|
+
const bashMatch = "bash" in hook && typeof hook.bash === "string" && hook.bash === expectedCommand;
|
|
15410
|
+
const powershellMatch = "powershell" in hook && typeof hook.powershell === "string" && hook.powershell === expectedCommand;
|
|
15411
|
+
return bashMatch || powershellMatch;
|
|
15412
|
+
});
|
|
15213
15413
|
}
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
|
|
15221
|
-
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
15414
|
+
/**
|
|
15415
|
+
* Ensures agent-shell's own commands are in an existing policy.json allow list.
|
|
15416
|
+
* Returns a discriminated union:
|
|
15417
|
+
* - `patched` with new content when entries were added
|
|
15418
|
+
* - `unchanged` when all entries are already present
|
|
15419
|
+
* - `invalid` when the file cannot be parsed or fails schema validation
|
|
15420
|
+
*/ function ensureAgentShellAllowed(content) {
|
|
15421
|
+
let parsed;
|
|
15422
|
+
try {
|
|
15423
|
+
parsed = JSON.parse(content);
|
|
15424
|
+
} catch {
|
|
15425
|
+
return {
|
|
15426
|
+
status: "invalid",
|
|
15427
|
+
reason: "JSON parse error"
|
|
15428
|
+
};
|
|
15429
|
+
}
|
|
15430
|
+
const result = PolicyConfigSchema.safeParse(parsed);
|
|
15431
|
+
if (!result.success) {
|
|
15432
|
+
return {
|
|
15433
|
+
status: "invalid",
|
|
15434
|
+
reason: "policy schema validation failed"
|
|
15435
|
+
};
|
|
15436
|
+
}
|
|
15437
|
+
const policy = result.data;
|
|
15438
|
+
const allow = policy.allow ? [
|
|
15439
|
+
...policy.allow
|
|
15440
|
+
] : [];
|
|
15441
|
+
const missing = AGENT_SHELL_ALLOW_ENTRIES.filter((entry)=>!allow.includes(entry));
|
|
15442
|
+
if (missing.length === 0) {
|
|
15443
|
+
return {
|
|
15444
|
+
status: "unchanged"
|
|
15445
|
+
};
|
|
15446
|
+
}
|
|
15447
|
+
allow.push(...missing);
|
|
15448
|
+
const patched = {
|
|
15449
|
+
...policy,
|
|
15450
|
+
allow
|
|
15451
|
+
};
|
|
15452
|
+
return {
|
|
15453
|
+
status: "patched",
|
|
15454
|
+
content: `${JSON.stringify(patched, null, 2)}\n`
|
|
15228
15455
|
};
|
|
15229
|
-
await writeEvent(eventsDir, sessionId, event, deps);
|
|
15230
15456
|
}
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15457
|
+
function detectExistingFeatures(config) {
|
|
15458
|
+
return {
|
|
15459
|
+
hasPreToolUse: hasHookCommand(config.hooks.preToolUse, AGENT_SHELL_POLICY_CHECK),
|
|
15460
|
+
hasPostToolUse: hasHookCommand(config.hooks.postToolUse, AGENT_SHELL_RECORD)
|
|
15235
15461
|
};
|
|
15236
|
-
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
|
|
15245
|
-
|
|
15246
|
-
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
|
|
15462
|
+
}
|
|
15463
|
+
async function loadExistingHooksConfig(hooksPath, deps) {
|
|
15464
|
+
try {
|
|
15465
|
+
const raw = await deps.readFile(hooksPath, "utf-8");
|
|
15466
|
+
const parsed = JSON.parse(raw);
|
|
15467
|
+
return {
|
|
15468
|
+
config: HooksConfigSchema.parse(parsed),
|
|
15469
|
+
error: false
|
|
15470
|
+
};
|
|
15471
|
+
} catch (err) {
|
|
15472
|
+
if (isPathNotFoundError(err)) {
|
|
15473
|
+
return {
|
|
15474
|
+
config: null,
|
|
15475
|
+
error: false
|
|
15476
|
+
};
|
|
15477
|
+
}
|
|
15478
|
+
deps.writeStderr(`agent-shell: failed to read existing hooks.json: ${sanitizeForStderr(err)}\n`);
|
|
15479
|
+
return {
|
|
15480
|
+
config: null,
|
|
15481
|
+
error: true
|
|
15482
|
+
};
|
|
15483
|
+
}
|
|
15484
|
+
}
|
|
15485
|
+
function hasExplicitFlags(flags) {
|
|
15486
|
+
return flags.flightRecorder || flags.policy || flags.noFlightRecorder || flags.noPolicy;
|
|
15487
|
+
}
|
|
15488
|
+
/**
|
|
15489
|
+
* Resolves feature selections in explicit-flag mode.
|
|
15490
|
+
* Only features with an explicit --flag are enabled; unspecified features
|
|
15491
|
+
* are not added (existing hooks are preserved via config merge).
|
|
15492
|
+
*/ function resolveExplicitFlagSelections(flags) {
|
|
15493
|
+
let enableFlightRecorder;
|
|
15494
|
+
let enablePolicy;
|
|
15495
|
+
if (flags.noFlightRecorder) {
|
|
15496
|
+
enableFlightRecorder = false;
|
|
15497
|
+
} else {
|
|
15498
|
+
enableFlightRecorder = flags.flightRecorder;
|
|
15499
|
+
}
|
|
15500
|
+
if (flags.noPolicy) {
|
|
15501
|
+
enablePolicy = false;
|
|
15502
|
+
} else {
|
|
15503
|
+
enablePolicy = flags.policy;
|
|
15504
|
+
}
|
|
15505
|
+
return {
|
|
15506
|
+
enableFlightRecorder,
|
|
15507
|
+
enablePolicy
|
|
15251
15508
|
};
|
|
15252
|
-
await writeEvent(eventsDir, sessionId, event, depsWithProjectRoot);
|
|
15253
15509
|
}
|
|
15254
|
-
|
|
15255
|
-
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15259
|
-
|
|
15260
|
-
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
15272
|
-
|
|
15273
|
-
|
|
15274
|
-
|
|
15510
|
+
async function validatePathContainment(subpath, repoRoot, deps) {
|
|
15511
|
+
const resolvedPath = (0,external_node_path_namespaceObject.resolve)(repoRoot, subpath);
|
|
15512
|
+
if (!isWithinProjectRoot(resolvedPath, repoRoot)) {
|
|
15513
|
+
deps.writeStderr(`agent-shell: path ${subpath} resolves outside repository root, aborting\n`);
|
|
15514
|
+
return false;
|
|
15515
|
+
}
|
|
15516
|
+
// Canonicalize repoRoot first — if this fails, the containment check
|
|
15517
|
+
// is impossible and we must abort.
|
|
15518
|
+
let realRepoRoot;
|
|
15519
|
+
try {
|
|
15520
|
+
realRepoRoot = await deps.realpath(repoRoot);
|
|
15521
|
+
} catch (err) {
|
|
15522
|
+
if (!isPathNotFoundError(err)) {
|
|
15523
|
+
throw err;
|
|
15524
|
+
}
|
|
15525
|
+
deps.writeStderr(`agent-shell: repository root ${sanitizeForStderr(repoRoot)} is unreachable or cannot be canonicalized, aborting\n`);
|
|
15526
|
+
return false;
|
|
15527
|
+
}
|
|
15528
|
+
try {
|
|
15529
|
+
const realPath = await deps.realpath(resolvedPath);
|
|
15530
|
+
if (!isWithinProjectRoot(realPath, realRepoRoot)) {
|
|
15531
|
+
deps.writeStderr(`agent-shell: path ${subpath} resolves outside repository root via symlink, aborting\n`);
|
|
15532
|
+
return false;
|
|
15533
|
+
}
|
|
15534
|
+
} catch (err) {
|
|
15535
|
+
if (!isPathNotFoundError(err)) {
|
|
15536
|
+
throw err;
|
|
15537
|
+
}
|
|
15538
|
+
// Target path doesn't exist yet, which is fine for new config files
|
|
15539
|
+
}
|
|
15540
|
+
return true;
|
|
15275
15541
|
}
|
|
15276
|
-
function
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
|
|
15542
|
+
async function writeConfigFile(repoRoot, subpath, content, deps) {
|
|
15543
|
+
const valid = await validatePathContainment(subpath, repoRoot, deps);
|
|
15544
|
+
if (!valid) {
|
|
15545
|
+
return false;
|
|
15546
|
+
}
|
|
15547
|
+
const filePath = (0,external_node_path_namespaceObject.join)(repoRoot, subpath);
|
|
15548
|
+
const parentDir = (0,external_node_path_namespaceObject.join)(repoRoot, HOOKS_PARENT);
|
|
15549
|
+
await deps.mkdir(parentDir, {
|
|
15550
|
+
recursive: true
|
|
15280
15551
|
});
|
|
15281
|
-
|
|
15282
|
-
|
|
15283
|
-
|
|
15552
|
+
// Post-mkdir containment recheck: verify the parent directory hasn't
|
|
15553
|
+
// been redirected via symlink created between the pre-check and mkdir.
|
|
15554
|
+
let realRepoRoot;
|
|
15284
15555
|
try {
|
|
15285
|
-
|
|
15286
|
-
|
|
15287
|
-
|
|
15288
|
-
|
|
15289
|
-
|
|
15290
|
-
|
|
15291
|
-
|
|
15292
|
-
|
|
15293
|
-
|
|
15294
|
-
|
|
15295
|
-
|
|
15296
|
-
|
|
15297
|
-
|
|
15298
|
-
timeoutHandle.unref();
|
|
15299
|
-
});
|
|
15300
|
-
try {
|
|
15301
|
-
await Promise.race([
|
|
15302
|
-
emission,
|
|
15303
|
-
timeout
|
|
15304
|
-
]);
|
|
15305
|
-
} finally{
|
|
15306
|
-
if (timeoutHandle !== undefined) {
|
|
15307
|
-
clearTimeout(timeoutHandle);
|
|
15308
|
-
}
|
|
15556
|
+
realRepoRoot = await deps.realpath(repoRoot);
|
|
15557
|
+
} catch (err) {
|
|
15558
|
+
if (!isPathNotFoundError(err)) {
|
|
15559
|
+
throw err;
|
|
15560
|
+
}
|
|
15561
|
+
deps.writeStderr(`agent-shell: repository root ${sanitizeForStderr(repoRoot)} is unreachable or cannot be canonicalized, aborting\n`);
|
|
15562
|
+
return false;
|
|
15563
|
+
}
|
|
15564
|
+
try {
|
|
15565
|
+
const realParent = await deps.realpath(parentDir);
|
|
15566
|
+
if (!isWithinProjectRoot(realParent, realRepoRoot)) {
|
|
15567
|
+
deps.writeStderr(`agent-shell: parent directory resolves outside repository root after mkdir, aborting\n`);
|
|
15568
|
+
return false;
|
|
15309
15569
|
}
|
|
15310
15570
|
} catch (err) {
|
|
15311
|
-
|
|
15571
|
+
if (!isPathNotFoundError(err)) {
|
|
15572
|
+
throw err;
|
|
15573
|
+
}
|
|
15312
15574
|
}
|
|
15313
|
-
|
|
15314
|
-
|
|
15575
|
+
// Atomic write: write to a temp file in the same directory, then rename.
|
|
15576
|
+
// rename() replaces the destination directory entry atomically — if the
|
|
15577
|
+
// target path is (or becomes) a symlink, rename replaces the symlink
|
|
15578
|
+
// itself rather than following it, closing the TOCTOU window between
|
|
15579
|
+
// validation and write.
|
|
15580
|
+
// Uses crypto.randomBytes for an unpredictable suffix and exclusive-create
|
|
15581
|
+
// flag ('wx' = O_CREAT | O_WRONLY | O_EXCL) so writeFile fails if a
|
|
15582
|
+
// symlink or file already exists at the temp path.
|
|
15583
|
+
const tmpSuffix = (0,external_node_crypto_namespaceObject.randomBytes)(8).toString("hex");
|
|
15584
|
+
const tmpPath = `${filePath}.${tmpSuffix}.tmp`;
|
|
15315
15585
|
try {
|
|
15316
|
-
|
|
15317
|
-
|
|
15586
|
+
await deps.writeFile(tmpPath, content, {
|
|
15587
|
+
flag: "wx"
|
|
15588
|
+
});
|
|
15589
|
+
await deps.rename(tmpPath, filePath);
|
|
15590
|
+
} catch (err) {
|
|
15591
|
+
// Best-effort cleanup of orphaned temp file on rename failure
|
|
15318
15592
|
try {
|
|
15319
|
-
|
|
15593
|
+
await deps.unlink(tmpPath);
|
|
15320
15594
|
} catch {
|
|
15321
|
-
|
|
15322
|
-
|
|
15323
|
-
return;
|
|
15595
|
+
// Ignore cleanup errors — the temp file may not exist if
|
|
15596
|
+
// writeFile failed, or the directory may be read-only.
|
|
15324
15597
|
}
|
|
15325
|
-
|
|
15326
|
-
|
|
15327
|
-
|
|
15328
|
-
|
|
15598
|
+
throw err;
|
|
15599
|
+
}
|
|
15600
|
+
deps.writeStdout(`Wrote ${subpath}\n`);
|
|
15601
|
+
return true;
|
|
15602
|
+
}
|
|
15603
|
+
async function handleInit(flags, deps) {
|
|
15604
|
+
const repoRoot = deps.getRepositoryRoot();
|
|
15605
|
+
const hooksPath = (0,external_node_path_namespaceObject.join)(repoRoot, init_command_HOOKS_SUBPATH);
|
|
15606
|
+
const { config: existingConfig, error: loadError } = await loadExistingHooksConfig(hooksPath, deps);
|
|
15607
|
+
if (loadError) {
|
|
15608
|
+
return false;
|
|
15609
|
+
}
|
|
15610
|
+
const existing = existingConfig ? detectExistingFeatures(existingConfig) : {
|
|
15611
|
+
hasPreToolUse: false,
|
|
15612
|
+
hasPostToolUse: false
|
|
15613
|
+
};
|
|
15614
|
+
if (existing.hasPreToolUse && existing.hasPostToolUse) {
|
|
15615
|
+
deps.writeStdout("All features already configured in hooks.json\n");
|
|
15616
|
+
return true;
|
|
15617
|
+
}
|
|
15618
|
+
let enableFlightRecorder;
|
|
15619
|
+
let enablePolicy;
|
|
15620
|
+
if (hasExplicitFlags(flags)) {
|
|
15621
|
+
// Non-interactive: apply explicit flags only — don't add unspecified features
|
|
15622
|
+
const selections = resolveExplicitFlagSelections(flags);
|
|
15623
|
+
enableFlightRecorder = selections.enableFlightRecorder && !existing.hasPostToolUse;
|
|
15624
|
+
enablePolicy = selections.enablePolicy && !existing.hasPreToolUse;
|
|
15625
|
+
// If the user requested features but they're all already configured, report no-op
|
|
15626
|
+
if (!enableFlightRecorder && !enablePolicy && (selections.enableFlightRecorder || selections.enablePolicy)) {
|
|
15627
|
+
deps.writeStdout("Requested features already configured in hooks.json; nothing to do\n");
|
|
15628
|
+
return true;
|
|
15329
15629
|
}
|
|
15330
|
-
|
|
15331
|
-
//
|
|
15332
|
-
|
|
15333
|
-
|
|
15334
|
-
|
|
15335
|
-
|
|
15336
|
-
|
|
15337
|
-
|
|
15338
|
-
deps.
|
|
15339
|
-
|
|
15630
|
+
} else if (!deps.isTty && deps.prompt === undefined) {
|
|
15631
|
+
// Non-TTY with no explicit flags: default to enabling all missing features
|
|
15632
|
+
const missing = [];
|
|
15633
|
+
enableFlightRecorder = !existing.hasPostToolUse;
|
|
15634
|
+
enablePolicy = !existing.hasPreToolUse;
|
|
15635
|
+
if (enableFlightRecorder) missing.push("flight recording");
|
|
15636
|
+
if (enablePolicy) missing.push("policy blocking");
|
|
15637
|
+
if (missing.length > 0) {
|
|
15638
|
+
deps.writeStderr(`agent-shell: non-interactive mode, auto-enabling: ${missing.join(", ")}\n`);
|
|
15639
|
+
}
|
|
15640
|
+
} else if (deps.prompt) {
|
|
15641
|
+
// Interactive: prompt for each missing feature
|
|
15642
|
+
enableFlightRecorder = false;
|
|
15643
|
+
enablePolicy = false;
|
|
15644
|
+
if (!existing.hasPostToolUse) {
|
|
15645
|
+
enableFlightRecorder = await deps.prompt("Enable flight recording to capture all agent tool usage?");
|
|
15646
|
+
}
|
|
15647
|
+
if (!existing.hasPreToolUse) {
|
|
15648
|
+
enablePolicy = await deps.prompt("Enable policy-based command blocking?");
|
|
15340
15649
|
}
|
|
15341
|
-
|
|
15342
|
-
//
|
|
15343
|
-
|
|
15344
|
-
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
|
|
15650
|
+
} else {
|
|
15651
|
+
// Fallback: enable all missing features
|
|
15652
|
+
enableFlightRecorder = !existing.hasPostToolUse;
|
|
15653
|
+
enablePolicy = !existing.hasPreToolUse;
|
|
15654
|
+
}
|
|
15655
|
+
if (!enableFlightRecorder && !enablePolicy) {
|
|
15656
|
+
deps.writeStdout("No features selected, nothing to do.\n");
|
|
15657
|
+
return true;
|
|
15658
|
+
}
|
|
15659
|
+
// Build the hooks config by merging agent-shell hooks into existing config,
|
|
15660
|
+
// preserving any other hooks (sessionStart, sessionEnd, userPromptSubmitted, etc.)
|
|
15661
|
+
const mergedConfig = existingConfig ? structuredClone(existingConfig) : {
|
|
15662
|
+
version: 1,
|
|
15663
|
+
hooks: {}
|
|
15664
|
+
};
|
|
15665
|
+
if (enablePolicy && !existing.hasPreToolUse) {
|
|
15666
|
+
const policyHook = {
|
|
15667
|
+
type: "command",
|
|
15668
|
+
bash: AGENT_SHELL_POLICY_CHECK,
|
|
15669
|
+
timeoutSec: 30
|
|
15670
|
+
};
|
|
15671
|
+
mergedConfig.hooks.preToolUse = [
|
|
15672
|
+
...mergedConfig.hooks.preToolUse ?? [],
|
|
15673
|
+
policyHook
|
|
15674
|
+
];
|
|
15675
|
+
}
|
|
15676
|
+
if (enableFlightRecorder && !existing.hasPostToolUse) {
|
|
15677
|
+
const recordHook = {
|
|
15678
|
+
type: "command",
|
|
15679
|
+
bash: AGENT_SHELL_RECORD,
|
|
15680
|
+
timeoutSec: 30
|
|
15681
|
+
};
|
|
15682
|
+
mergedConfig.hooks.postToolUse = [
|
|
15683
|
+
...mergedConfig.hooks.postToolUse ?? [],
|
|
15684
|
+
recordHook
|
|
15685
|
+
];
|
|
15686
|
+
}
|
|
15687
|
+
const hooksContent = `${JSON.stringify(mergedConfig, null, 2)}\n`;
|
|
15688
|
+
const hooksWritten = await writeConfigFile(repoRoot, init_command_HOOKS_SUBPATH, hooksContent, deps);
|
|
15689
|
+
if (!hooksWritten) {
|
|
15690
|
+
return false;
|
|
15691
|
+
}
|
|
15692
|
+
// Generate policy.json if policy is being enabled and no policy file exists yet.
|
|
15693
|
+
// If the file already exists, ensure agent-shell's own commands are in the allow list
|
|
15694
|
+
// so that the preToolUse hook doesn't block the sibling postToolUse hook.
|
|
15695
|
+
if (enablePolicy) {
|
|
15696
|
+
const policyPath = (0,external_node_path_namespaceObject.join)(repoRoot, init_command_POLICY_SUBPATH);
|
|
15697
|
+
let existingContent = null;
|
|
15698
|
+
try {
|
|
15699
|
+
existingContent = await deps.readFile(policyPath, "utf-8");
|
|
15700
|
+
} catch (error) {
|
|
15701
|
+
if (!isPathNotFoundError(error)) {
|
|
15702
|
+
throw error;
|
|
15703
|
+
}
|
|
15704
|
+
}
|
|
15705
|
+
if (existingContent === null) {
|
|
15706
|
+
deps.writeStdout("Scanning project...\n");
|
|
15707
|
+
const scanResult = await deps.scanProject(repoRoot);
|
|
15708
|
+
const policy = generatePolicy(scanResult);
|
|
15709
|
+
const policyContent = `${JSON.stringify(policy, null, 2)}\n`;
|
|
15710
|
+
const policyWritten = await writeConfigFile(repoRoot, init_command_POLICY_SUBPATH, policyContent, deps);
|
|
15711
|
+
if (!policyWritten) {
|
|
15712
|
+
return false;
|
|
15713
|
+
}
|
|
15714
|
+
} else {
|
|
15715
|
+
// Ensure agent-shell commands are in the existing allow list
|
|
15716
|
+
const patchResult = ensureAgentShellAllowed(existingContent);
|
|
15717
|
+
switch(patchResult.status){
|
|
15718
|
+
case "patched":
|
|
15719
|
+
{
|
|
15720
|
+
const policyWritten = await writeConfigFile(repoRoot, init_command_POLICY_SUBPATH, patchResult.content, deps);
|
|
15721
|
+
if (!policyWritten) {
|
|
15722
|
+
return false;
|
|
15723
|
+
}
|
|
15724
|
+
break;
|
|
15725
|
+
}
|
|
15726
|
+
case "unchanged":
|
|
15727
|
+
deps.writeStdout("Policy already exists with agent-shell rules; skipping policy.json generation.\n");
|
|
15728
|
+
break;
|
|
15729
|
+
case "invalid":
|
|
15730
|
+
deps.writeStderr(`agent-shell: existing policy.json is invalid (${sanitizeForStderr(patchResult.reason)}), regenerating\n`);
|
|
15731
|
+
deps.writeStdout("Scanning project...\n");
|
|
15732
|
+
{
|
|
15733
|
+
const scanResult = await deps.scanProject(repoRoot);
|
|
15734
|
+
const policy = generatePolicy(scanResult);
|
|
15735
|
+
const policyContent = `${JSON.stringify(policy, null, 2)}\n`;
|
|
15736
|
+
const policyWritten = await writeConfigFile(repoRoot, init_command_POLICY_SUBPATH, policyContent, deps);
|
|
15737
|
+
if (!policyWritten) {
|
|
15738
|
+
return false;
|
|
15739
|
+
}
|
|
15740
|
+
}
|
|
15741
|
+
break;
|
|
15742
|
+
default:
|
|
15743
|
+
{
|
|
15744
|
+
const _exhaustive = patchResult;
|
|
15745
|
+
throw new Error(`Unhandled policy patch status: ${_exhaustive.status}`);
|
|
15746
|
+
}
|
|
15747
|
+
}
|
|
15348
15748
|
}
|
|
15349
|
-
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15749
|
+
}
|
|
15750
|
+
// Print summary
|
|
15751
|
+
const actions = [];
|
|
15752
|
+
if (enableFlightRecorder) actions.push("flight recording");
|
|
15753
|
+
if (enablePolicy) actions.push("policy blocking");
|
|
15754
|
+
deps.writeStdout(`\nEnabled: ${actions.join(", ")}\n`);
|
|
15755
|
+
return true;
|
|
15756
|
+
}
|
|
15757
|
+
|
|
15758
|
+
;// CONCATENATED MODULE: ./src/log/format.ts
|
|
15759
|
+
|
|
15760
|
+
function formatDuration(ms) {
|
|
15761
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
15762
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
15763
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
15764
|
+
}
|
|
15765
|
+
function formatTimestamp(iso) {
|
|
15766
|
+
const d = new Date(iso);
|
|
15767
|
+
return d.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "");
|
|
15768
|
+
}
|
|
15769
|
+
function truncateCommand(command, maxLen) {
|
|
15770
|
+
if (command.length <= maxLen) return command;
|
|
15771
|
+
return `${command.slice(0, maxLen - 3)}...`;
|
|
15772
|
+
}
|
|
15773
|
+
function padRight(str, width) {
|
|
15774
|
+
if (str.length >= width) return str;
|
|
15775
|
+
return str + " ".repeat(width - str.length);
|
|
15776
|
+
}
|
|
15777
|
+
const COL_TIMESTAMP = 21;
|
|
15778
|
+
const COL_SCRIPT = 9;
|
|
15779
|
+
const COL_ACTOR = 13;
|
|
15780
|
+
const COL_EXIT = 6;
|
|
15781
|
+
const COL_DURATION = 10;
|
|
15782
|
+
const MAX_COMMAND_LEN = 50;
|
|
15783
|
+
function formatEventsTable(events) {
|
|
15784
|
+
const header = padRight("TIMESTAMP", COL_TIMESTAMP) + padRight("SCRIPT", COL_SCRIPT) + padRight("ACTOR", COL_ACTOR) + padRight("EXIT", COL_EXIT) + padRight("DURATION", COL_DURATION) + "COMMAND";
|
|
15785
|
+
const rows = events.map((event)=>{
|
|
15786
|
+
const timestamp = formatTimestamp(event.timestamp);
|
|
15787
|
+
const script = event.event === "script_end" ? event.script ?? "-" : event.event === "tool_use" ? sanitizeOutput(event.tool_name) : "-";
|
|
15788
|
+
const actor = event.actor;
|
|
15789
|
+
const exitCode = event.event === "script_end" ? String(event.exit_code) : "-";
|
|
15790
|
+
const duration = event.event === "script_end" ? formatDuration(event.duration_ms) : "-";
|
|
15791
|
+
const command = truncateCommand(event.command, MAX_COMMAND_LEN);
|
|
15792
|
+
return padRight(timestamp, COL_TIMESTAMP) + padRight(script, COL_SCRIPT) + padRight(actor, COL_ACTOR) + padRight(exitCode, COL_EXIT) + padRight(duration, COL_DURATION) + command;
|
|
15793
|
+
});
|
|
15794
|
+
return [
|
|
15795
|
+
header,
|
|
15796
|
+
...rows
|
|
15797
|
+
].join("\n");
|
|
15798
|
+
}
|
|
15799
|
+
function formatSessionsTable(sessions) {
|
|
15800
|
+
const colSession = 11;
|
|
15801
|
+
const colFirstEvent = 21;
|
|
15802
|
+
const colLastEvent = 21;
|
|
15803
|
+
const colEvents = 9;
|
|
15804
|
+
const header = padRight("SESSION", colSession) + padRight("FIRST EVENT", colFirstEvent) + padRight("LAST EVENT", colLastEvent) + padRight("EVENTS", colEvents) + "ACTORS";
|
|
15805
|
+
const rows = sessions.map((s)=>{
|
|
15806
|
+
const sessionTrunc = s.sessionId.slice(0, 8);
|
|
15807
|
+
const firstEvent = formatTimestamp(s.firstEvent);
|
|
15808
|
+
const lastEvent = formatTimestamp(s.lastEvent);
|
|
15809
|
+
const eventCount = String(s.eventCount);
|
|
15810
|
+
const actors = s.actors.join(", ");
|
|
15811
|
+
return padRight(sessionTrunc, colSession) + padRight(firstEvent, colFirstEvent) + padRight(lastEvent, colLastEvent) + padRight(eventCount, colEvents) + actors;
|
|
15812
|
+
});
|
|
15813
|
+
return [
|
|
15814
|
+
header,
|
|
15815
|
+
...rows
|
|
15816
|
+
].join("\n");
|
|
15817
|
+
}
|
|
15818
|
+
function formatEventsJson(events) {
|
|
15819
|
+
return JSON.stringify(events, null, 2);
|
|
15820
|
+
}
|
|
15821
|
+
|
|
15822
|
+
;// CONCATENATED MODULE: ./src/log/query.ts
|
|
15823
|
+
// biome-ignore-all lint/style/useNamingConvention: telemetry schema uses snake_case field names
|
|
15824
|
+
|
|
15825
|
+
|
|
15826
|
+
|
|
15827
|
+
const DURATION_PATTERN = /^(\d+)([mhd])$/;
|
|
15828
|
+
const MAX_LINE_BYTES = 65_536;
|
|
15829
|
+
const MAX_LINES_PER_FILE = 100_000;
|
|
15830
|
+
const UNIT_MS = {
|
|
15831
|
+
m: 60 * 1000,
|
|
15832
|
+
h: 60 * 60 * 1000,
|
|
15833
|
+
d: 24 * 60 * 60 * 1000
|
|
15834
|
+
};
|
|
15835
|
+
function parseDuration(duration) {
|
|
15836
|
+
const match = DURATION_PATTERN.exec(duration);
|
|
15837
|
+
if (!match) {
|
|
15838
|
+
throw new Error(`Invalid duration format: "${duration}". Expected format: <number><unit> where unit is m, h, or d`);
|
|
15839
|
+
}
|
|
15840
|
+
const value = Number.parseInt(match[1], 10);
|
|
15841
|
+
if (value <= 0) {
|
|
15842
|
+
throw new Error(`Duration must be a positive value, got: "${duration}"`);
|
|
15843
|
+
}
|
|
15844
|
+
const unit = match[2];
|
|
15845
|
+
return value * UNIT_MS[unit];
|
|
15846
|
+
}
|
|
15847
|
+
async function resolveReadEventsDir(env, deps) {
|
|
15848
|
+
const projectRoot = deps.cwd();
|
|
15849
|
+
const defaultDir = (0,external_node_path_namespaceObject.join)(projectRoot, ".agent-shell", "events");
|
|
15850
|
+
const logDir = env.AGENTSHELL_LOG_DIR;
|
|
15851
|
+
if (logDir !== undefined && logDir !== "") {
|
|
15852
|
+
const projectRootReal = await deps.realpath(projectRoot);
|
|
15853
|
+
const candidate = (0,external_node_path_namespaceObject.resolve)(projectRoot, logDir);
|
|
15854
|
+
if (!isWithinProjectRoot(candidate, projectRoot) && !isWithinProjectRoot(candidate, projectRootReal)) {
|
|
15855
|
+
return {
|
|
15856
|
+
dir: "",
|
|
15857
|
+
error: "AGENTSHELL_LOG_DIR resolves outside project root"
|
|
15858
|
+
};
|
|
15353
15859
|
}
|
|
15354
|
-
let
|
|
15860
|
+
let resolved;
|
|
15355
15861
|
try {
|
|
15356
|
-
|
|
15357
|
-
} catch
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
|
|
15362
|
-
|
|
15363
|
-
|
|
15364
|
-
|
|
15365
|
-
return;
|
|
15366
|
-
}
|
|
15367
|
-
const obj = parsedArgs;
|
|
15368
|
-
if (!Object.hasOwn(obj, "command")) {
|
|
15369
|
-
deps.writeStdout(denyResponse("Missing command field in toolArgs"));
|
|
15370
|
-
return;
|
|
15862
|
+
resolved = await deps.realpath(candidate);
|
|
15863
|
+
} catch (err) {
|
|
15864
|
+
if (isPathNotFoundError(err)) {
|
|
15865
|
+
return {
|
|
15866
|
+
dir: "",
|
|
15867
|
+
error: "AGENTSHELL_LOG_DIR does not exist or is not a directory"
|
|
15868
|
+
};
|
|
15869
|
+
}
|
|
15870
|
+
throw err;
|
|
15371
15871
|
}
|
|
15372
|
-
if (
|
|
15373
|
-
|
|
15374
|
-
|
|
15872
|
+
if (!isWithinProjectRoot(resolved, projectRootReal)) {
|
|
15873
|
+
return {
|
|
15874
|
+
dir: "",
|
|
15875
|
+
error: "AGENTSHELL_LOG_DIR resolves outside project root"
|
|
15876
|
+
};
|
|
15375
15877
|
}
|
|
15376
|
-
|
|
15377
|
-
|
|
15378
|
-
|
|
15379
|
-
deps.writeStdout(responseJson);
|
|
15380
|
-
await tryEmitTelemetry(deps, command.trim(), result.decision, result.matchedRule);
|
|
15381
|
-
} catch (err) {
|
|
15382
|
-
deps.writeStderr(`agent-shell: unexpected error: ${sanitizeForStderr(err)}\n`);
|
|
15383
|
-
deps.writeStdout(denyResponse("Internal error during policy evaluation"));
|
|
15878
|
+
return {
|
|
15879
|
+
dir: resolved
|
|
15880
|
+
};
|
|
15384
15881
|
}
|
|
15882
|
+
return {
|
|
15883
|
+
dir: defaultDir
|
|
15884
|
+
};
|
|
15385
15885
|
}
|
|
15386
|
-
|
|
15387
|
-
|
|
15388
|
-
|
|
15389
|
-
|
|
15390
|
-
|
|
15391
|
-
}
|
|
15392
|
-
|
|
15393
|
-
return
|
|
15886
|
+
function findMostRecentFile(fileMtimes) {
|
|
15887
|
+
let mostRecent = fileMtimes[0];
|
|
15888
|
+
for(let i = 1; i < fileMtimes.length; i++){
|
|
15889
|
+
if (fileMtimes[i].mtimeMs > mostRecent.mtimeMs) {
|
|
15890
|
+
mostRecent = fileMtimes[i];
|
|
15891
|
+
}
|
|
15892
|
+
}
|
|
15893
|
+
return mostRecent.file;
|
|
15394
15894
|
}
|
|
15395
|
-
function
|
|
15396
|
-
|
|
15895
|
+
function matchesFilters(event, filters, cutoffMs) {
|
|
15896
|
+
if (filters.actor !== undefined && event.actor !== filters.actor) {
|
|
15897
|
+
return false;
|
|
15898
|
+
}
|
|
15899
|
+
if (filters.failures && !(event.event === "script_end" && event.exit_code !== 0)) {
|
|
15900
|
+
return false;
|
|
15901
|
+
}
|
|
15902
|
+
if (filters.script !== undefined && !(event.event === "script_end" && event.script === filters.script)) {
|
|
15903
|
+
return false;
|
|
15904
|
+
}
|
|
15905
|
+
if (cutoffMs !== undefined) {
|
|
15906
|
+
const eventMs = new Date(event.timestamp).getTime();
|
|
15907
|
+
if (eventMs < cutoffMs) {
|
|
15908
|
+
return false;
|
|
15909
|
+
}
|
|
15910
|
+
}
|
|
15911
|
+
return true;
|
|
15397
15912
|
}
|
|
15398
|
-
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
15402
|
-
|
|
15403
|
-
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
|
|
15410
|
-
|
|
15411
|
-
|
|
15412
|
-
|
|
15413
|
-
|
|
15414
|
-
## Available Tools
|
|
15415
|
-
|
|
15416
|
-
### MCP Tools (lousy-agents server)
|
|
15417
|
-
|
|
15418
|
-
- **discover_feedback_loops**: Discover npm scripts and CLI tools from workflows, mapped to SDLC phases (test, build, lint, format, security). Returns structured results grouped by phase. **Start here.**
|
|
15419
|
-
- **discover_environment**: Discover environment config files (mise.toml, .nvmrc, .python-version, etc.) and detect package managers.
|
|
15420
|
-
|
|
15421
|
-
### Custom Tools
|
|
15422
|
-
|
|
15423
|
-
- **read_project_file**: Read any file in the repository (truncated at 100KB). Use to inspect configs discovered via MCP tools.
|
|
15424
|
-
- **validate_allow_rule**: Check whether a proposed command is safe for the allow list (rejects commands containing shell metacharacters).
|
|
15425
|
-
|
|
15426
|
-
## Response Format
|
|
15427
|
-
|
|
15428
|
-
After exploring, respond with **only** a JSON object matching this exact schema — no markdown fences, no explanation:
|
|
15429
|
-
|
|
15430
|
-
\`\`\`json
|
|
15431
|
-
{
|
|
15432
|
-
"additionalAllowRules": ["<command>", ...],
|
|
15433
|
-
"suggestions": ["<human-readable suggestion>", ...]
|
|
15913
|
+
function parseLine(line) {
|
|
15914
|
+
if (Buffer.byteLength(line, "utf-8") > MAX_LINE_BYTES) {
|
|
15915
|
+
return undefined;
|
|
15916
|
+
}
|
|
15917
|
+
let parsed;
|
|
15918
|
+
try {
|
|
15919
|
+
parsed = JSON.parse(line);
|
|
15920
|
+
} catch {
|
|
15921
|
+
return undefined;
|
|
15922
|
+
}
|
|
15923
|
+
const result = ScriptEventSchema.safeParse(parsed);
|
|
15924
|
+
if (!result.success) {
|
|
15925
|
+
return undefined;
|
|
15926
|
+
}
|
|
15927
|
+
return result.data;
|
|
15434
15928
|
}
|
|
15435
|
-
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
|
|
15929
|
+
async function queryEvents(eventsDir, filters, deps) {
|
|
15930
|
+
const allFiles = await deps.readdir(eventsDir);
|
|
15931
|
+
const jsonlFiles = allFiles.filter((f)=>f.endsWith(".jsonl"));
|
|
15932
|
+
if (jsonlFiles.length === 0) {
|
|
15933
|
+
return {
|
|
15934
|
+
events: [],
|
|
15935
|
+
truncatedFiles: []
|
|
15936
|
+
};
|
|
15937
|
+
}
|
|
15938
|
+
let filesToRead;
|
|
15939
|
+
if (filters.last === undefined) {
|
|
15940
|
+
const fileMtimes = await Promise.all(jsonlFiles.map(async (file)=>{
|
|
15941
|
+
const filePath = (0,external_node_path_namespaceObject.join)(eventsDir, file);
|
|
15942
|
+
const s = await deps.stat(filePath);
|
|
15943
|
+
return {
|
|
15944
|
+
file,
|
|
15945
|
+
mtimeMs: s.mtimeMs
|
|
15946
|
+
};
|
|
15947
|
+
}));
|
|
15948
|
+
filesToRead = [
|
|
15949
|
+
findMostRecentFile(fileMtimes)
|
|
15950
|
+
];
|
|
15951
|
+
} else {
|
|
15952
|
+
filesToRead = jsonlFiles;
|
|
15953
|
+
}
|
|
15954
|
+
const cutoffMs = filters.last ? Date.now() - parseDuration(filters.last) : undefined;
|
|
15955
|
+
const events = [];
|
|
15956
|
+
const truncatedFiles = [];
|
|
15957
|
+
for (const file of filesToRead){
|
|
15958
|
+
const filePath = (0,external_node_path_namespaceObject.join)(eventsDir, file);
|
|
15959
|
+
let lineCount = 0;
|
|
15960
|
+
for await (const line of deps.readFileLines(filePath)){
|
|
15961
|
+
if (lineCount >= MAX_LINES_PER_FILE) {
|
|
15962
|
+
deps.writeStderr(`agent-shell: file ${file} exceeds ${MAX_LINES_PER_FILE} lines, truncating\n`);
|
|
15963
|
+
truncatedFiles.push(file);
|
|
15964
|
+
break;
|
|
15965
|
+
}
|
|
15966
|
+
lineCount++;
|
|
15967
|
+
const event = parseLine(line);
|
|
15968
|
+
if (event === undefined) {
|
|
15969
|
+
continue;
|
|
15970
|
+
}
|
|
15971
|
+
if (matchesFilters(event, filters, cutoffMs)) {
|
|
15972
|
+
events.push(event);
|
|
15973
|
+
}
|
|
15974
|
+
}
|
|
15975
|
+
}
|
|
15976
|
+
return {
|
|
15977
|
+
events,
|
|
15978
|
+
truncatedFiles
|
|
15979
|
+
};
|
|
15980
|
+
}
|
|
15981
|
+
async function listSessions(eventsDir, deps) {
|
|
15982
|
+
const allFiles = await deps.readdir(eventsDir);
|
|
15983
|
+
const jsonlFiles = allFiles.filter((f)=>f.endsWith(".jsonl"));
|
|
15984
|
+
if (jsonlFiles.length === 0) {
|
|
15985
|
+
return [];
|
|
15986
|
+
}
|
|
15987
|
+
const summaries = [];
|
|
15988
|
+
for (const file of jsonlFiles){
|
|
15989
|
+
const sessionId = file.replace(/\.jsonl$/, "");
|
|
15990
|
+
const filePath = (0,external_node_path_namespaceObject.join)(eventsDir, file);
|
|
15991
|
+
let firstEvent;
|
|
15992
|
+
let lastEvent;
|
|
15993
|
+
let eventCount = 0;
|
|
15994
|
+
const actorSet = new Set();
|
|
15995
|
+
for await (const line of deps.readFileLines(filePath)){
|
|
15996
|
+
let parsed;
|
|
15997
|
+
try {
|
|
15998
|
+
parsed = JSON.parse(line);
|
|
15999
|
+
} catch {
|
|
16000
|
+
continue;
|
|
16001
|
+
}
|
|
16002
|
+
const result = ScriptEventSchema.safeParse(parsed);
|
|
16003
|
+
if (!result.success) {
|
|
16004
|
+
continue;
|
|
16005
|
+
}
|
|
16006
|
+
const event = result.data;
|
|
16007
|
+
eventCount++;
|
|
16008
|
+
actorSet.add(event.actor);
|
|
16009
|
+
if (firstEvent === undefined || event.timestamp < firstEvent) {
|
|
16010
|
+
firstEvent = event.timestamp;
|
|
16011
|
+
}
|
|
16012
|
+
if (lastEvent === undefined || event.timestamp > lastEvent) {
|
|
16013
|
+
lastEvent = event.timestamp;
|
|
16014
|
+
}
|
|
16015
|
+
}
|
|
16016
|
+
if (eventCount > 0 && firstEvent !== undefined && lastEvent !== undefined) {
|
|
16017
|
+
summaries.push({
|
|
16018
|
+
sessionId,
|
|
16019
|
+
firstEvent,
|
|
16020
|
+
lastEvent,
|
|
16021
|
+
eventCount,
|
|
16022
|
+
actors: [
|
|
16023
|
+
...actorSet
|
|
16024
|
+
]
|
|
16025
|
+
});
|
|
16026
|
+
}
|
|
16027
|
+
}
|
|
16028
|
+
summaries.sort((a, b)=>b.lastEvent.localeCompare(a.lastEvent));
|
|
16029
|
+
return summaries;
|
|
15439
16030
|
}
|
|
15440
|
-
/**
|
|
15441
|
-
* Builds the user prompt containing project-specific scan results
|
|
15442
|
-
* and Socratic questions to guide exploration.
|
|
15443
|
-
*/ function buildAnalysisPrompt(scanResult, repoRoot) {
|
|
15444
|
-
const scriptsList = scanResult.scripts.length > 0 ? formatScriptsSummary(scanResult.scripts) : " (none found)";
|
|
15445
|
-
const workflowList = scanResult.workflowCommands.length > 0 ? formatWorkflowSummary(scanResult.workflowCommands) : " (none found)";
|
|
15446
|
-
const miseList = scanResult.miseTasks.length > 0 ? formatMiseTasksSummary(scanResult.miseTasks) : " (none found)";
|
|
15447
|
-
const languagesList = scanResult.languages.length > 0 ? scanResult.languages.join(", ") : "(none detected)";
|
|
15448
|
-
const safeRepoRoot = sanitizePromptValue(repoRoot);
|
|
15449
|
-
return `# Project Analysis Request
|
|
15450
|
-
|
|
15451
|
-
## Static Analysis Results
|
|
15452
|
-
|
|
15453
|
-
We have already discovered the following from static file analysis:
|
|
15454
16031
|
|
|
15455
|
-
|
|
15456
|
-
- **Detected languages**: ${languagesList}
|
|
16032
|
+
;// CONCATENATED MODULE: ./src/log/index.ts
|
|
15457
16033
|
|
|
15458
|
-
### npm scripts (from package.json)
|
|
15459
|
-
${scriptsList}
|
|
15460
16034
|
|
|
15461
|
-
### GitHub Actions workflow commands
|
|
15462
|
-
${workflowList}
|
|
15463
16035
|
|
|
15464
|
-
### Mise tasks (from mise.toml)
|
|
15465
|
-
${miseList}
|
|
15466
16036
|
|
|
15467
|
-
## Questions to Explore
|
|
15468
16037
|
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
|
|
15473
|
-
|
|
16038
|
+
function parseLogArgs(args) {
|
|
16039
|
+
const options = {
|
|
16040
|
+
failures: false,
|
|
16041
|
+
listSessions: false,
|
|
16042
|
+
json: false
|
|
16043
|
+
};
|
|
16044
|
+
let i = 0;
|
|
16045
|
+
while(i < args.length){
|
|
16046
|
+
const arg = args[i];
|
|
16047
|
+
switch(arg){
|
|
16048
|
+
case "--last":
|
|
16049
|
+
if (i + 1 < args.length) {
|
|
16050
|
+
options.last = args[++i];
|
|
16051
|
+
} else {
|
|
16052
|
+
options.errors = options.errors ?? [];
|
|
16053
|
+
options.errors.push("--last requires a value (e.g., 30m, 1h, 1d)");
|
|
16054
|
+
}
|
|
16055
|
+
break;
|
|
16056
|
+
case "--actor":
|
|
16057
|
+
if (i + 1 < args.length) {
|
|
16058
|
+
options.actor = args[++i];
|
|
16059
|
+
} else {
|
|
16060
|
+
options.errors = options.errors ?? [];
|
|
16061
|
+
options.errors.push("--actor requires a value");
|
|
16062
|
+
}
|
|
16063
|
+
break;
|
|
16064
|
+
case "--failures":
|
|
16065
|
+
options.failures = true;
|
|
16066
|
+
break;
|
|
16067
|
+
case "--script":
|
|
16068
|
+
if (i + 1 < args.length) {
|
|
16069
|
+
options.script = args[++i];
|
|
16070
|
+
} else {
|
|
16071
|
+
options.errors = options.errors ?? [];
|
|
16072
|
+
options.errors.push("--script requires a value");
|
|
16073
|
+
}
|
|
16074
|
+
break;
|
|
16075
|
+
case "--list-sessions":
|
|
16076
|
+
options.listSessions = true;
|
|
16077
|
+
break;
|
|
16078
|
+
case "--json":
|
|
16079
|
+
options.json = true;
|
|
16080
|
+
break;
|
|
16081
|
+
}
|
|
16082
|
+
i++;
|
|
16083
|
+
}
|
|
16084
|
+
return options;
|
|
15474
16085
|
}
|
|
15475
|
-
|
|
15476
|
-
|
|
15477
|
-
|
|
15478
|
-
|
|
15479
|
-
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
15483
|
-
|
|
15484
|
-
|
|
15485
|
-
|
|
15486
|
-
|
|
15487
|
-
|
|
15488
|
-
|
|
15489
|
-
|
|
15490
|
-
|
|
15491
|
-
|
|
15492
|
-
|
|
15493
|
-
|
|
15494
|
-
|
|
15495
|
-
|
|
15496
|
-
if (
|
|
16086
|
+
function createDefaultQueryDeps() {
|
|
16087
|
+
return {
|
|
16088
|
+
readdir: (path)=>(0,promises_namespaceObject.readdir)(path),
|
|
16089
|
+
stat: (path)=>(0,promises_namespaceObject.stat)(path).then((s)=>({
|
|
16090
|
+
mtimeMs: s.mtimeMs
|
|
16091
|
+
})),
|
|
16092
|
+
realpath: (path)=>(0,promises_namespaceObject.realpath)(path),
|
|
16093
|
+
cwd: ()=>process.cwd(),
|
|
16094
|
+
readFileLines: (path)=>(0,external_node_readline_namespaceObject.createInterface)({
|
|
16095
|
+
input: (0,external_node_fs_namespaceObject.createReadStream)(path, {
|
|
16096
|
+
encoding: "utf-8"
|
|
16097
|
+
})
|
|
16098
|
+
}),
|
|
16099
|
+
writeStderr: (msg)=>{
|
|
16100
|
+
process.stderr.write(msg);
|
|
16101
|
+
}
|
|
16102
|
+
};
|
|
16103
|
+
}
|
|
16104
|
+
async function runLog(args) {
|
|
16105
|
+
const options = parseLogArgs(args);
|
|
16106
|
+
const deps = createDefaultQueryDeps();
|
|
16107
|
+
if (options.errors && options.errors.length > 0) {
|
|
16108
|
+
for (const err of options.errors){
|
|
16109
|
+
process.stderr.write(`agent-shell: ${err}\n`);
|
|
16110
|
+
}
|
|
16111
|
+
return 1;
|
|
16112
|
+
}
|
|
16113
|
+
const { dir, error } = await resolveReadEventsDir(process.env, deps);
|
|
16114
|
+
if (error) {
|
|
16115
|
+
process.stderr.write(`agent-shell: ${error}\n`);
|
|
16116
|
+
return 1;
|
|
16117
|
+
}
|
|
16118
|
+
if (options.last) {
|
|
16119
|
+
try {
|
|
16120
|
+
parseDuration(options.last);
|
|
16121
|
+
} catch (err) {
|
|
16122
|
+
process.stderr.write(`agent-shell: ${err.message}\n`);
|
|
16123
|
+
return 1;
|
|
16124
|
+
}
|
|
16125
|
+
}
|
|
15497
16126
|
try {
|
|
15498
|
-
|
|
15499
|
-
const cjsResolved = projectRequire.resolve(packageName);
|
|
15500
|
-
const esmUrl = findEsmEntry(cjsResolved, packageName);
|
|
15501
|
-
return esmUrl ?? (0,external_node_url_namespaceObject.pathToFileURL)(cjsResolved).href;
|
|
16127
|
+
await deps.readdir(dir);
|
|
15502
16128
|
} catch {
|
|
15503
|
-
|
|
16129
|
+
process.stdout.write("No events recorded yet.\n\n");
|
|
16130
|
+
process.stdout.write("To enable instrumentation, add to your .npmrc:\n");
|
|
16131
|
+
process.stdout.write(" script-shell=./node_modules/.bin/agent-shell\n");
|
|
16132
|
+
return 0;
|
|
16133
|
+
}
|
|
16134
|
+
if (options.listSessions) {
|
|
16135
|
+
const sessions = await listSessions(dir, deps);
|
|
16136
|
+
if (sessions.length === 0) {
|
|
16137
|
+
process.stdout.write("No sessions found.\n");
|
|
16138
|
+
return 0;
|
|
16139
|
+
}
|
|
16140
|
+
process.stdout.write(formatSessionsTable(sessions));
|
|
16141
|
+
process.stdout.write("\n");
|
|
16142
|
+
return 0;
|
|
16143
|
+
}
|
|
16144
|
+
const result = await queryEvents(dir, {
|
|
16145
|
+
actor: options.actor,
|
|
16146
|
+
failures: options.failures || undefined,
|
|
16147
|
+
script: options.script,
|
|
16148
|
+
last: options.last
|
|
16149
|
+
}, deps);
|
|
16150
|
+
if (result.events.length === 0) {
|
|
16151
|
+
process.stdout.write("No matching events found.\n");
|
|
16152
|
+
return 0;
|
|
16153
|
+
}
|
|
16154
|
+
if (options.json) {
|
|
16155
|
+
process.stdout.write(formatEventsJson(result.events));
|
|
16156
|
+
process.stdout.write("\n");
|
|
16157
|
+
} else {
|
|
16158
|
+
process.stdout.write(formatEventsTable(result.events));
|
|
16159
|
+
process.stdout.write("\n");
|
|
15504
16160
|
}
|
|
16161
|
+
return 0;
|
|
15505
16162
|
}
|
|
15506
|
-
|
|
15507
|
-
|
|
15508
|
-
|
|
15509
|
-
|
|
15510
|
-
|
|
15511
|
-
|
|
15512
|
-
|
|
15513
|
-
const
|
|
15514
|
-
|
|
15515
|
-
|
|
15516
|
-
const esmEntry = extractEsmEntry(pkg);
|
|
15517
|
-
const full = (0,external_node_path_namespaceObject.resolve)(dir, esmEntry);
|
|
15518
|
-
try {
|
|
15519
|
-
return (0,external_node_url_namespaceObject.pathToFileURL)((0,external_node_fs_namespaceObject.realpathSync)(full)).href;
|
|
15520
|
-
} catch {
|
|
15521
|
-
return null;
|
|
15522
|
-
}
|
|
16163
|
+
|
|
16164
|
+
;// CONCATENATED MODULE: ./src/mode.ts
|
|
16165
|
+
const MODEL_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
16166
|
+
function parsePolicyInitOptions(args) {
|
|
16167
|
+
const options = {};
|
|
16168
|
+
for (const arg of args.slice(2)){
|
|
16169
|
+
if (arg.startsWith("--model=")) {
|
|
16170
|
+
const value = arg.slice("--model=".length);
|
|
16171
|
+
if (value.length > 0 && value.length <= 128 && MODEL_PATTERN.test(value)) {
|
|
16172
|
+
options.model = value;
|
|
15523
16173
|
}
|
|
15524
|
-
}
|
|
15525
|
-
/* no package.json at this level */ }
|
|
15526
|
-
const parent = (0,external_node_path_namespaceObject.dirname)(dir);
|
|
15527
|
-
if (parent === dir) return null;
|
|
15528
|
-
dir = parent;
|
|
16174
|
+
}
|
|
15529
16175
|
}
|
|
16176
|
+
return options;
|
|
15530
16177
|
}
|
|
15531
|
-
|
|
15532
|
-
|
|
15533
|
-
|
|
15534
|
-
|
|
15535
|
-
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
|
|
16178
|
+
function parseInitOptions(args) {
|
|
16179
|
+
const options = {
|
|
16180
|
+
flightRecorder: false,
|
|
16181
|
+
policy: false,
|
|
16182
|
+
noFlightRecorder: false,
|
|
16183
|
+
noPolicy: false,
|
|
16184
|
+
unknownArgs: []
|
|
16185
|
+
};
|
|
16186
|
+
for (const arg of args.slice(1)){
|
|
16187
|
+
switch(arg){
|
|
16188
|
+
case "--flight-recorder":
|
|
16189
|
+
options.flightRecorder = true;
|
|
16190
|
+
break;
|
|
16191
|
+
case "--policy":
|
|
16192
|
+
options.policy = true;
|
|
16193
|
+
break;
|
|
16194
|
+
case "--no-flight-recorder":
|
|
16195
|
+
options.noFlightRecorder = true;
|
|
16196
|
+
break;
|
|
16197
|
+
case "--no-policy":
|
|
16198
|
+
options.noPolicy = true;
|
|
16199
|
+
break;
|
|
16200
|
+
default:
|
|
16201
|
+
options.unknownArgs.push(arg);
|
|
16202
|
+
break;
|
|
16203
|
+
}
|
|
15539
16204
|
}
|
|
15540
|
-
return
|
|
16205
|
+
return options;
|
|
15541
16206
|
}
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
|
|
15545
|
-
|
|
15546
|
-
|
|
15547
|
-
|
|
15548
|
-
|
|
15549
|
-
|
|
15550
|
-
|
|
15551
|
-
|
|
15552
|
-
|
|
15553
|
-
|
|
15554
|
-
|
|
15555
|
-
|
|
15556
|
-
|
|
15557
|
-
|
|
15558
|
-
|
|
16207
|
+
function resolveMode(args, env) {
|
|
16208
|
+
const firstArg = args[0];
|
|
16209
|
+
if (firstArg === "policy-check") return {
|
|
16210
|
+
type: "policy-check"
|
|
16211
|
+
};
|
|
16212
|
+
if (firstArg === "record") return {
|
|
16213
|
+
type: "record"
|
|
16214
|
+
};
|
|
16215
|
+
if (firstArg === "init") {
|
|
16216
|
+
const options = parseInitOptions(args);
|
|
16217
|
+
return {
|
|
16218
|
+
type: "init",
|
|
16219
|
+
...options
|
|
16220
|
+
};
|
|
16221
|
+
}
|
|
16222
|
+
if (firstArg === "policy" && args[1] === "--init") {
|
|
16223
|
+
const options = parsePolicyInitOptions(args);
|
|
16224
|
+
return {
|
|
16225
|
+
type: "policy-init",
|
|
16226
|
+
model: options.model
|
|
16227
|
+
};
|
|
16228
|
+
}
|
|
16229
|
+
if (env.AGENTSHELL_PASSTHROUGH === "1") {
|
|
16230
|
+
return {
|
|
16231
|
+
type: "passthrough",
|
|
16232
|
+
args
|
|
16233
|
+
};
|
|
15559
16234
|
}
|
|
15560
|
-
return
|
|
16235
|
+
if (firstArg === "--version") return {
|
|
16236
|
+
type: "version"
|
|
16237
|
+
};
|
|
16238
|
+
if (firstArg === "-c" && args[1]) return {
|
|
16239
|
+
type: "shim",
|
|
16240
|
+
command: args[1]
|
|
16241
|
+
};
|
|
16242
|
+
if (firstArg === "log") return {
|
|
16243
|
+
type: "log"
|
|
16244
|
+
};
|
|
16245
|
+
return {
|
|
16246
|
+
type: "usage"
|
|
16247
|
+
};
|
|
15561
16248
|
}
|
|
15562
16249
|
|
|
15563
|
-
;// CONCATENATED MODULE: ./src/
|
|
15564
|
-
|
|
15565
|
-
|
|
16250
|
+
;// CONCATENATED MODULE: ./src/policy.ts
|
|
15566
16251
|
|
|
15567
16252
|
|
|
15568
16253
|
|
|
15569
16254
|
|
|
15570
|
-
const
|
|
15571
|
-
const MAX_FILE_READ_BYTES = 102_400;
|
|
15572
|
-
const AnalysisResponseSchema = schemas_object({
|
|
15573
|
-
additionalAllowRules: schemas_array(schemas_string().max(512)).max(100),
|
|
15574
|
-
suggestions: schemas_array(schemas_string().max(1024)).max(100)
|
|
15575
|
-
});
|
|
16255
|
+
const DEFAULT_POLICY_SUBPATH = ".github/hooks/agent-shell/policy.json";
|
|
15576
16256
|
/**
|
|
15577
|
-
*
|
|
15578
|
-
* does not escape the root via traversal (e.g. `../../etc/passwd`).
|
|
16257
|
+
* Glob matcher supporting only `*` wildcards.
|
|
15579
16258
|
*
|
|
15580
|
-
*
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
16259
|
+
* Splits the rule on `*` into literal segments and checks that each
|
|
16260
|
+
* segment appears in the command in order. O(n·m) worst case where
|
|
16261
|
+
* n = command length and m = rule length, with no exponential
|
|
16262
|
+
* backtracking (unlike regex `.*` quantifiers).
|
|
16263
|
+
*/ function matchesRule(command, rule) {
|
|
16264
|
+
const segments = rule.split("*");
|
|
16265
|
+
if (segments.length === 1) {
|
|
16266
|
+
return command === rule;
|
|
15587
16267
|
}
|
|
15588
|
-
|
|
16268
|
+
const prefixSegment = segments[0];
|
|
16269
|
+
const suffixSegment = segments[segments.length - 1];
|
|
16270
|
+
const innerSegments = segments.slice(1, -1);
|
|
16271
|
+
if (!command.startsWith(prefixSegment)) {
|
|
16272
|
+
return false;
|
|
16273
|
+
}
|
|
16274
|
+
let cursor = prefixSegment.length;
|
|
16275
|
+
for (const segment of innerSegments){
|
|
16276
|
+
const index = command.indexOf(segment, cursor);
|
|
16277
|
+
if (index === -1) {
|
|
16278
|
+
return false;
|
|
16279
|
+
}
|
|
16280
|
+
cursor = index + segment.length;
|
|
16281
|
+
}
|
|
16282
|
+
const suffixStart = command.length - suffixSegment.length;
|
|
16283
|
+
return suffixStart >= cursor && command.endsWith(suffixSegment);
|
|
15589
16284
|
}
|
|
15590
|
-
|
|
15591
|
-
|
|
15592
|
-
* and byte-level truncation. Extracted for testability.
|
|
15593
|
-
*/ async function readProjectFileSafe(repoRoot, pathArg) {
|
|
15594
|
-
if (pathArg.length === 0) {
|
|
16285
|
+
function evaluatePolicy(policy, command) {
|
|
16286
|
+
if (policy === null) {
|
|
15595
16287
|
return {
|
|
15596
|
-
|
|
16288
|
+
decision: "allow",
|
|
16289
|
+
matchedRule: null
|
|
15597
16290
|
};
|
|
15598
16291
|
}
|
|
15599
|
-
const
|
|
15600
|
-
if (
|
|
16292
|
+
const trimmed = command.trim();
|
|
16293
|
+
if (SHELL_METACHAR_PATTERN.test(trimmed)) {
|
|
15601
16294
|
return {
|
|
15602
|
-
|
|
16295
|
+
decision: "deny",
|
|
16296
|
+
matchedRule: null
|
|
15603
16297
|
};
|
|
15604
16298
|
}
|
|
15605
|
-
|
|
15606
|
-
|
|
15607
|
-
const [realRoot, realPath] = await Promise.all([
|
|
15608
|
-
(0,promises_namespaceObject.realpath)(root),
|
|
15609
|
-
(0,promises_namespaceObject.realpath)(safePath)
|
|
15610
|
-
]);
|
|
15611
|
-
const realPrefix = realRoot === "/" ? "/" : `${realRoot}/`;
|
|
15612
|
-
if (!realPath.startsWith(realPrefix) && realPath !== realRoot) {
|
|
16299
|
+
for (const rule of policy.deny){
|
|
16300
|
+
if (matchesRule(trimmed, rule)) {
|
|
15613
16301
|
return {
|
|
15614
|
-
|
|
16302
|
+
decision: "deny",
|
|
16303
|
+
matchedRule: rule
|
|
16304
|
+
};
|
|
16305
|
+
}
|
|
16306
|
+
}
|
|
16307
|
+
if (policy.allow !== undefined) {
|
|
16308
|
+
const matchesAny = policy.allow.some((rule)=>matchesRule(trimmed, rule));
|
|
16309
|
+
if (!matchesAny) {
|
|
16310
|
+
return {
|
|
16311
|
+
decision: "deny",
|
|
16312
|
+
matchedRule: null
|
|
15615
16313
|
};
|
|
15616
16314
|
}
|
|
15617
|
-
const fileBuffer = await (0,promises_namespaceObject.readFile)(realPath);
|
|
15618
|
-
const isTruncated = fileBuffer.length > MAX_FILE_READ_BYTES;
|
|
15619
|
-
const limitedBuffer = isTruncated ? fileBuffer.subarray(0, MAX_FILE_READ_BYTES) : fileBuffer;
|
|
15620
|
-
return {
|
|
15621
|
-
content: limitedBuffer.toString("utf-8"),
|
|
15622
|
-
truncated: isTruncated
|
|
15623
|
-
};
|
|
15624
|
-
} catch {
|
|
15625
|
-
return {
|
|
15626
|
-
error: "File not found or unreadable"
|
|
15627
|
-
};
|
|
15628
16315
|
}
|
|
16316
|
+
return {
|
|
16317
|
+
decision: "allow",
|
|
16318
|
+
matchedRule: null
|
|
16319
|
+
};
|
|
15629
16320
|
}
|
|
15630
16321
|
/**
|
|
15631
|
-
*
|
|
15632
|
-
*
|
|
15633
|
-
*
|
|
15634
|
-
*/ function
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
type: "object",
|
|
15639
|
-
properties: {
|
|
15640
|
-
path: {
|
|
15641
|
-
type: "string",
|
|
15642
|
-
description: "Relative path from repository root"
|
|
15643
|
-
}
|
|
15644
|
-
},
|
|
15645
|
-
required: [
|
|
15646
|
-
"path"
|
|
15647
|
-
]
|
|
15648
|
-
},
|
|
15649
|
-
skipPermission: true,
|
|
15650
|
-
handler: (args)=>readProjectFileSafe(repoRoot, args.path ?? "")
|
|
16322
|
+
* Escapes ASCII control characters in a path before embedding it in an error
|
|
16323
|
+
* message. Prevents log/terminal injection when the path originates from an
|
|
16324
|
+
* environment variable (e.g. AGENTSHELL_POLICY_PATH).
|
|
16325
|
+
*/ function sanitizePath(path) {
|
|
16326
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentionally matching control characters for sanitization
|
|
16327
|
+
return path.replace(/[\u0000-\u001f\u007f]/g, (ch)=>{
|
|
16328
|
+
return `\\x${ch.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
|
15651
16329
|
});
|
|
15652
|
-
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
15656
|
-
|
|
15657
|
-
|
|
15658
|
-
type: "string",
|
|
15659
|
-
description: "The command to validate as a policy rule"
|
|
15660
|
-
}
|
|
15661
|
-
},
|
|
15662
|
-
required: [
|
|
15663
|
-
"command"
|
|
15664
|
-
]
|
|
15665
|
-
},
|
|
15666
|
-
skipPermission: true,
|
|
15667
|
-
handler: async (args)=>{
|
|
15668
|
-
const command = args.command ?? "";
|
|
15669
|
-
if (isSafeCommand(command)) {
|
|
15670
|
-
return {
|
|
15671
|
-
safe: true,
|
|
15672
|
-
reason: "No shell metacharacters detected"
|
|
15673
|
-
};
|
|
15674
|
-
}
|
|
15675
|
-
const normalized = command.trim();
|
|
15676
|
-
if (normalized.length === 0) {
|
|
15677
|
-
return {
|
|
15678
|
-
safe: false,
|
|
15679
|
-
reason: "Empty or whitespace-only commands are not allowed in policy rules"
|
|
15680
|
-
};
|
|
15681
|
-
}
|
|
16330
|
+
}
|
|
16331
|
+
function resolvePolicyPath(env, repoRoot) {
|
|
16332
|
+
const override = env.AGENTSHELL_POLICY_PATH;
|
|
16333
|
+
if (override !== undefined && override !== "") {
|
|
16334
|
+
if ((0,external_node_path_namespaceObject.isAbsolute)(override)) {
|
|
16335
|
+
// Absolute path — use as-is (will be validated after realpath)
|
|
15682
16336
|
return {
|
|
15683
|
-
|
|
15684
|
-
|
|
16337
|
+
path: override,
|
|
16338
|
+
isOverride: true
|
|
15685
16339
|
};
|
|
15686
16340
|
}
|
|
15687
|
-
|
|
15688
|
-
|
|
15689
|
-
|
|
15690
|
-
|
|
15691
|
-
|
|
16341
|
+
// Relative path — resolve relative to repo root
|
|
16342
|
+
return {
|
|
16343
|
+
path: (0,external_node_path_namespaceObject.join)(repoRoot, override),
|
|
16344
|
+
isOverride: true
|
|
16345
|
+
};
|
|
16346
|
+
}
|
|
16347
|
+
return {
|
|
16348
|
+
path: (0,external_node_path_namespaceObject.join)(repoRoot, DEFAULT_POLICY_SUBPATH),
|
|
16349
|
+
isOverride: false
|
|
16350
|
+
};
|
|
15692
16351
|
}
|
|
15693
|
-
|
|
15694
|
-
|
|
15695
|
-
|
|
15696
|
-
|
|
15697
|
-
|
|
15698
|
-
|
|
15699
|
-
|
|
15700
|
-
|
|
15701
|
-
|
|
15702
|
-
|
|
15703
|
-
|
|
15704
|
-
const sdkPath = resolveSdkPath(repoRoot, "@github/copilot-sdk");
|
|
15705
|
-
const { CopilotClient, defineTool, approveAll } = sdkPath ? await import(/* webpackIgnore: true */ sdkPath) : await __webpack_require__.e(/* import() */ "300").then(__webpack_require__.bind(__webpack_require__, 791));
|
|
15706
|
-
importSucceeded = true;
|
|
15707
|
-
const client = new CopilotClient();
|
|
15708
|
-
const tools = createCustomTools(repoRoot, defineTool);
|
|
15709
|
-
try {
|
|
15710
|
-
await client.start();
|
|
15711
|
-
const session = await client.createSession({
|
|
15712
|
-
model,
|
|
15713
|
-
tools,
|
|
15714
|
-
onPermissionRequest: approveAll,
|
|
15715
|
-
systemMessage: {
|
|
15716
|
-
content: buildSystemMessage()
|
|
15717
|
-
},
|
|
15718
|
-
mcpServers: {
|
|
15719
|
-
"lousy-agents": {
|
|
15720
|
-
type: "local",
|
|
15721
|
-
command: "npx",
|
|
15722
|
-
args: [
|
|
15723
|
-
"-y",
|
|
15724
|
-
"@lousy-agents/mcp"
|
|
15725
|
-
],
|
|
15726
|
-
cwd: repoRoot,
|
|
15727
|
-
tools: [
|
|
15728
|
-
"discover_feedback_loops",
|
|
15729
|
-
"discover_environment"
|
|
15730
|
-
]
|
|
15731
|
-
}
|
|
15732
|
-
}
|
|
15733
|
-
});
|
|
15734
|
-
try {
|
|
15735
|
-
const prompt = buildAnalysisPrompt(scanResult, repoRoot);
|
|
15736
|
-
const response = await session.sendAndWait({
|
|
15737
|
-
prompt
|
|
15738
|
-
});
|
|
15739
|
-
const data = typeof response?.data === "object" && response.data !== null ? response.data : undefined;
|
|
15740
|
-
const content = data !== undefined && "content" in data && typeof data.content === "string" ? data.content : "";
|
|
15741
|
-
return parseAnalysisResponse(content);
|
|
15742
|
-
} finally{
|
|
15743
|
-
await session.disconnect();
|
|
15744
|
-
}
|
|
15745
|
-
} finally{
|
|
15746
|
-
await client.stop();
|
|
15747
|
-
}
|
|
15748
|
-
} catch (err) {
|
|
15749
|
-
if (process.env.AGENT_SHELL_COPILOT_DEBUG) {
|
|
15750
|
-
if (importSucceeded) {
|
|
15751
|
-
writeStderr(`agent-shell: Copilot analysis failed — ${sanitizeForStderr(err)}\n`);
|
|
15752
|
-
} else {
|
|
15753
|
-
writeStderr("agent-shell: Copilot SDK not available — using static analysis only\n");
|
|
16352
|
+
async function loadPolicy(env, deps) {
|
|
16353
|
+
const rawRepoRoot = deps.getRepositoryRoot();
|
|
16354
|
+
const repoRoot = await deps.realpath(rawRepoRoot);
|
|
16355
|
+
const { path: candidatePath, isOverride } = resolvePolicyPath(env, repoRoot);
|
|
16356
|
+
let resolvedPath;
|
|
16357
|
+
try {
|
|
16358
|
+
resolvedPath = await deps.realpath(candidatePath);
|
|
16359
|
+
} catch (error) {
|
|
16360
|
+
if (isPathNotFoundError(error)) {
|
|
16361
|
+
if (isOverride) {
|
|
16362
|
+
throw new Error(`Policy override path does not exist: ${sanitizePath(candidatePath)}`);
|
|
15754
16363
|
}
|
|
16364
|
+
return null;
|
|
15755
16365
|
}
|
|
15756
|
-
|
|
16366
|
+
throw error;
|
|
15757
16367
|
}
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
* Finds the first well-formed JSON object in `content` using brace-balancing,
|
|
15761
|
-
* rather than a greedy regex that could capture extra trailing braces and
|
|
15762
|
-
* fail JSON.parse on valid responses that include preamble or postamble text.
|
|
15763
|
-
*/ function extractFirstJsonObject(content) {
|
|
15764
|
-
const start = content.indexOf("{");
|
|
15765
|
-
if (start === -1) {
|
|
15766
|
-
return null;
|
|
16368
|
+
if (!isWithinProjectRoot(resolvedPath, repoRoot)) {
|
|
16369
|
+
throw new Error(`Policy file path resolves outside the repository root: ${sanitizePath(resolvedPath)}`);
|
|
15767
16370
|
}
|
|
15768
|
-
let
|
|
15769
|
-
|
|
15770
|
-
|
|
15771
|
-
|
|
15772
|
-
|
|
15773
|
-
|
|
15774
|
-
|
|
15775
|
-
continue;
|
|
15776
|
-
}
|
|
15777
|
-
if (ch === "\\") {
|
|
15778
|
-
if (inString) {
|
|
15779
|
-
escaping = true;
|
|
15780
|
-
}
|
|
15781
|
-
continue;
|
|
15782
|
-
}
|
|
15783
|
-
if (ch === '"') {
|
|
15784
|
-
inString = !inString;
|
|
15785
|
-
continue;
|
|
15786
|
-
}
|
|
15787
|
-
if (!inString) {
|
|
15788
|
-
if (ch === "{") {
|
|
15789
|
-
depth += 1;
|
|
15790
|
-
} else if (ch === "}") {
|
|
15791
|
-
depth -= 1;
|
|
15792
|
-
if (depth === 0) {
|
|
15793
|
-
return content.slice(start, i + 1);
|
|
15794
|
-
}
|
|
16371
|
+
let content;
|
|
16372
|
+
try {
|
|
16373
|
+
content = await deps.readFile(resolvedPath, "utf-8");
|
|
16374
|
+
} catch (error) {
|
|
16375
|
+
if (isPathNotFoundError(error)) {
|
|
16376
|
+
if (isOverride) {
|
|
16377
|
+
throw new Error(`Policy override path does not exist: ${sanitizePath(resolvedPath)}`);
|
|
15795
16378
|
}
|
|
16379
|
+
return null;
|
|
15796
16380
|
}
|
|
16381
|
+
throw error;
|
|
15797
16382
|
}
|
|
15798
|
-
|
|
15799
|
-
}
|
|
15800
|
-
function parseAnalysisResponse(content) {
|
|
15801
|
-
const jsonText = extractFirstJsonObject(content);
|
|
15802
|
-
if (!jsonText) {
|
|
15803
|
-
return null;
|
|
15804
|
-
}
|
|
16383
|
+
let parsed;
|
|
15805
16384
|
try {
|
|
15806
|
-
|
|
15807
|
-
return AnalysisResponseSchema.parse(parsed);
|
|
16385
|
+
parsed = JSON.parse(content);
|
|
15808
16386
|
} catch {
|
|
15809
|
-
|
|
16387
|
+
throw new Error(`Invalid JSON in policy file ${sanitizePath(resolvedPath)}: file exists but contains malformed JSON`);
|
|
15810
16388
|
}
|
|
16389
|
+
return PolicyConfigSchema.parse(parsed);
|
|
15811
16390
|
}
|
|
15812
16391
|
|
|
15813
|
-
;// CONCATENATED MODULE: ./src/
|
|
15814
|
-
|
|
16392
|
+
;// CONCATENATED MODULE: ./src/actor.ts
|
|
16393
|
+
// Best-effort agent detection env vars (Phase 1):
|
|
16394
|
+
// - CLAUDE_CODE: Set by Claude Code (`CLAUDE_CODE=1`) in its shell sessions
|
|
16395
|
+
// - COPILOT_AGENT: Set by GitHub Copilot coding agent in its shell sessions
|
|
16396
|
+
// - COPILOT_CLI: Set by GitHub Copilot CLI (`COPILOT_CLI=1`) in its shell sessions
|
|
16397
|
+
// - COPILOT_CLI_BINARY_VERSION: Set by GitHub Copilot CLI (e.g. `COPILOT_CLI_BINARY_VERSION=1.0.4`)
|
|
16398
|
+
// If these prove incorrect, the detection rule should be removed entirely.
|
|
16399
|
+
/**
|
|
16400
|
+
* Determines who initiated a script execution based on environment variables.
|
|
16401
|
+
*
|
|
16402
|
+
* Detection priority (first match wins):
|
|
16403
|
+
* 1. Explicit override via AGENTSHELL_ACTOR
|
|
16404
|
+
* 2. CI detection via GITHUB_ACTIONS
|
|
16405
|
+
* 3. Known coding agent detection (Claude Code, GitHub Copilot)
|
|
16406
|
+
* 4. Fallback to "human"
|
|
16407
|
+
*/ function detectActor(env) {
|
|
16408
|
+
const override = env.AGENTSHELL_ACTOR;
|
|
16409
|
+
if (override !== undefined && override !== "") {
|
|
16410
|
+
return override;
|
|
16411
|
+
}
|
|
16412
|
+
if (env.GITHUB_ACTIONS === "true") {
|
|
16413
|
+
return "ci";
|
|
16414
|
+
}
|
|
16415
|
+
if (env.CLAUDE_CODE !== undefined && env.CLAUDE_CODE !== "") {
|
|
16416
|
+
return "claude-code";
|
|
16417
|
+
}
|
|
16418
|
+
if (env.COPILOT_AGENT !== undefined && env.COPILOT_AGENT !== "") {
|
|
16419
|
+
return "copilot";
|
|
16420
|
+
}
|
|
16421
|
+
if (env.COPILOT_CLI !== undefined && env.COPILOT_CLI !== "" || env.COPILOT_CLI_BINARY_VERSION !== undefined && env.COPILOT_CLI_BINARY_VERSION !== "") {
|
|
16422
|
+
return "copilot";
|
|
16423
|
+
}
|
|
16424
|
+
return "human";
|
|
16425
|
+
}
|
|
15815
16426
|
|
|
15816
|
-
|
|
15817
|
-
const
|
|
15818
|
-
const
|
|
15819
|
-
|
|
15820
|
-
|
|
15821
|
-
|
|
15822
|
-
|
|
15823
|
-
"
|
|
15824
|
-
"
|
|
15825
|
-
|
|
15826
|
-
|
|
15827
|
-
|
|
15828
|
-
|
|
15829
|
-
"
|
|
15830
|
-
|
|
15831
|
-
|
|
15832
|
-
|
|
15833
|
-
|
|
15834
|
-
|
|
15835
|
-
|
|
15836
|
-
|
|
15837
|
-
|
|
15838
|
-
|
|
16427
|
+
;// CONCATENATED MODULE: ./src/env-capture.ts
|
|
16428
|
+
const TAG_PREFIX = "AGENTSHELL_TAG_";
|
|
16429
|
+
const MAX_VALUE_BYTES = 1024;
|
|
16430
|
+
const MAX_TAGS = 50;
|
|
16431
|
+
const TRUNCATION_SUFFIX = "…[truncated]";
|
|
16432
|
+
const PROTOTYPE_POLLUTION_KEYS = new Set([
|
|
16433
|
+
"__proto__",
|
|
16434
|
+
"constructor",
|
|
16435
|
+
"prototype"
|
|
16436
|
+
]);
|
|
16437
|
+
const ALLOWLIST_PREFIXES = [
|
|
16438
|
+
"npm_lifecycle_",
|
|
16439
|
+
"github_",
|
|
16440
|
+
"agentshell_"
|
|
16441
|
+
];
|
|
16442
|
+
const ALLOWLIST_EXACT = new Set([
|
|
16443
|
+
"npm_package_name",
|
|
16444
|
+
"npm_package_version",
|
|
16445
|
+
"node_env",
|
|
16446
|
+
"ci"
|
|
16447
|
+
]);
|
|
16448
|
+
const BLOCKLIST_PATTERNS = [
|
|
16449
|
+
"secret",
|
|
16450
|
+
"token",
|
|
16451
|
+
"key",
|
|
16452
|
+
"password",
|
|
16453
|
+
"credential",
|
|
16454
|
+
"auth"
|
|
16455
|
+
];
|
|
16456
|
+
function isAllowlisted(name) {
|
|
16457
|
+
const lower = name.toLowerCase();
|
|
16458
|
+
if (ALLOWLIST_EXACT.has(lower)) {
|
|
16459
|
+
return true;
|
|
15839
16460
|
}
|
|
16461
|
+
return ALLOWLIST_PREFIXES.some((prefix)=>lower.startsWith(prefix));
|
|
15840
16462
|
}
|
|
15841
|
-
|
|
15842
|
-
|
|
15843
|
-
|
|
15844
|
-
|
|
15845
|
-
|
|
15846
|
-
|
|
16463
|
+
function isBlocklisted(name) {
|
|
16464
|
+
const lower = name.toLowerCase();
|
|
16465
|
+
return BLOCKLIST_PATTERNS.some((pattern)=>lower.includes(pattern));
|
|
16466
|
+
}
|
|
16467
|
+
function isTagVariable(name) {
|
|
16468
|
+
return name.startsWith(TAG_PREFIX);
|
|
16469
|
+
}
|
|
16470
|
+
function truncateValue(value) {
|
|
16471
|
+
const bytes = Buffer.byteLength(value, "utf-8");
|
|
16472
|
+
if (bytes <= MAX_VALUE_BYTES) {
|
|
16473
|
+
return {
|
|
16474
|
+
value,
|
|
16475
|
+
truncated: false
|
|
16476
|
+
};
|
|
16477
|
+
}
|
|
16478
|
+
// Codepoint-aware truncation: iterate Unicode codepoints to prevent
|
|
16479
|
+
// splitting multi-byte UTF-8 sequences at arbitrary byte boundaries
|
|
16480
|
+
let byteCount = 0;
|
|
16481
|
+
let truncated = "";
|
|
16482
|
+
for (const ch of value){
|
|
16483
|
+
const charBytes = Buffer.byteLength(ch, "utf-8");
|
|
16484
|
+
if (byteCount + charBytes > MAX_VALUE_BYTES) break;
|
|
16485
|
+
byteCount += charBytes;
|
|
16486
|
+
truncated += ch;
|
|
15847
16487
|
}
|
|
16488
|
+
return {
|
|
16489
|
+
value: `${truncated}${TRUNCATION_SUFFIX}`,
|
|
16490
|
+
truncated: true
|
|
16491
|
+
};
|
|
15848
16492
|
}
|
|
15849
|
-
|
|
15850
|
-
const
|
|
15851
|
-
|
|
15852
|
-
|
|
16493
|
+
function captureEnv(env) {
|
|
16494
|
+
const result = Object.create(null);
|
|
16495
|
+
let anyTruncated = false;
|
|
16496
|
+
for (const [name, value] of Object.entries(env)){
|
|
16497
|
+
if (value === undefined) continue;
|
|
16498
|
+
if (!isAllowlisted(name)) continue;
|
|
16499
|
+
if (isBlocklisted(name)) continue;
|
|
16500
|
+
if (isTagVariable(name)) continue;
|
|
16501
|
+
const { value: finalValue, truncated } = truncateValue(value);
|
|
16502
|
+
result[name] = finalValue;
|
|
16503
|
+
if (truncated) anyTruncated = true;
|
|
15853
16504
|
}
|
|
15854
|
-
|
|
15855
|
-
|
|
15856
|
-
if (fileStat.size > MAX_FILE_SIZE_BYTES) {
|
|
15857
|
-
return [];
|
|
15858
|
-
}
|
|
15859
|
-
const content = await (0,promises_namespaceObject.readFile)(packageJsonPath, "utf-8");
|
|
15860
|
-
const parsed = JSON.parse(content);
|
|
15861
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15862
|
-
return [];
|
|
15863
|
-
}
|
|
15864
|
-
const pkg = parsed;
|
|
15865
|
-
const scripts = pkg.scripts;
|
|
15866
|
-
if (scripts === null || scripts === undefined || typeof scripts !== "object" || Array.isArray(scripts)) {
|
|
15867
|
-
return [];
|
|
15868
|
-
}
|
|
15869
|
-
const result = [];
|
|
15870
|
-
for (const [name, command] of Object.entries(scripts)){
|
|
15871
|
-
if (typeof command === "string") {
|
|
15872
|
-
result.push({
|
|
15873
|
-
name,
|
|
15874
|
-
command
|
|
15875
|
-
});
|
|
15876
|
-
}
|
|
15877
|
-
}
|
|
15878
|
-
return result;
|
|
15879
|
-
} catch {
|
|
15880
|
-
return [];
|
|
16505
|
+
if (anyTruncated) {
|
|
16506
|
+
result._env_truncated = "true";
|
|
15881
16507
|
}
|
|
16508
|
+
return result;
|
|
15882
16509
|
}
|
|
15883
|
-
|
|
15884
|
-
const
|
|
15885
|
-
|
|
15886
|
-
|
|
16510
|
+
function captureTags(env) {
|
|
16511
|
+
const result = Object.create(null);
|
|
16512
|
+
let anyTruncatedOrDiscarded = false;
|
|
16513
|
+
const entries = [];
|
|
16514
|
+
for (const [name, value] of Object.entries(env)){
|
|
16515
|
+
if (value === undefined) continue;
|
|
16516
|
+
if (!name.startsWith(TAG_PREFIX)) continue;
|
|
16517
|
+
const tagKey = name.slice(TAG_PREFIX.length).toLowerCase();
|
|
16518
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(tagKey)) continue;
|
|
16519
|
+
entries.push([
|
|
16520
|
+
tagKey,
|
|
16521
|
+
value
|
|
16522
|
+
]);
|
|
16523
|
+
}
|
|
16524
|
+
entries.sort((a, b)=>a[0].localeCompare(b[0]));
|
|
16525
|
+
if (entries.length > MAX_TAGS) {
|
|
16526
|
+
anyTruncatedOrDiscarded = true;
|
|
16527
|
+
entries.length = MAX_TAGS;
|
|
15887
16528
|
}
|
|
15888
|
-
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
return [];
|
|
16529
|
+
for (const [key, rawValue] of entries){
|
|
16530
|
+
const { value, truncated } = truncateValue(rawValue);
|
|
16531
|
+
result[key] = value;
|
|
16532
|
+
if (truncated) anyTruncatedOrDiscarded = true;
|
|
15893
16533
|
}
|
|
15894
|
-
|
|
15895
|
-
|
|
15896
|
-
|
|
16534
|
+
if (anyTruncatedOrDiscarded) {
|
|
16535
|
+
result._tags_truncated = "true";
|
|
16536
|
+
}
|
|
16537
|
+
return result;
|
|
16538
|
+
}
|
|
16539
|
+
|
|
16540
|
+
;// CONCATENATED MODULE: ./src/telemetry.ts
|
|
16541
|
+
// biome-ignore-all lint/style/useNamingConvention: telemetry schema uses snake_case field names
|
|
16542
|
+
|
|
16543
|
+
|
|
16544
|
+
|
|
16545
|
+
|
|
16546
|
+
|
|
16547
|
+
const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
16548
|
+
const DEFAULT_EVENTS_SUBDIR = ".agent-shell/events";
|
|
16549
|
+
async function realpathExistingAncestor(targetPath, deps) {
|
|
16550
|
+
let current = targetPath;
|
|
16551
|
+
while(true){
|
|
15897
16552
|
try {
|
|
15898
|
-
|
|
15899
|
-
|
|
15900
|
-
if (
|
|
15901
|
-
|
|
15902
|
-
|
|
15903
|
-
|
|
15904
|
-
|
|
15905
|
-
allCommands.push(...commands);
|
|
15906
|
-
} catch {}
|
|
16553
|
+
return await deps.realpath(current);
|
|
16554
|
+
} catch (err) {
|
|
16555
|
+
if (!isPathNotFoundError(err)) throw err;
|
|
16556
|
+
const parent = (0,external_node_path_namespaceObject.dirname)(current);
|
|
16557
|
+
if (parent === current) return null;
|
|
16558
|
+
current = parent;
|
|
16559
|
+
}
|
|
15907
16560
|
}
|
|
15908
|
-
return [
|
|
15909
|
-
...new Set(allCommands)
|
|
15910
|
-
].filter(isUsefulCommand);
|
|
15911
16561
|
}
|
|
15912
|
-
|
|
15913
|
-
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
|
|
15919
|
-
|
|
15920
|
-
|
|
15921
|
-
|
|
15922
|
-
/^\w+="?\$\{\{/,
|
|
15923
|
-
/^[A-Z_]+=("[^"]*"|'[^']*'|\S+)\s*\\?$/,
|
|
15924
|
-
/^(echo|printf)\s/,
|
|
15925
|
-
/^(cd|mkdir|rm|test)\s/,
|
|
15926
|
-
/^>\s/,
|
|
15927
|
-
/^(else|fi|done|esac)$/,
|
|
15928
|
-
/^\w+=\$\?$/,
|
|
15929
|
-
/^\\$/,
|
|
15930
|
-
/^'[^']*'\s*\\?$/,
|
|
15931
|
-
/^"[^"]*"\s*\\?$/,
|
|
15932
|
-
/^node\s+"?\$/
|
|
15933
|
-
];
|
|
15934
|
-
function isUsefulCommand(cmd) {
|
|
15935
|
-
if (cmd.length < 3) return false;
|
|
15936
|
-
if (cmd.endsWith(")") && !cmd.includes("(")) return false;
|
|
15937
|
-
return !SHELL_NOISE_PATTERNS.some((pattern)=>pattern.test(cmd));
|
|
16562
|
+
function resolveSessionId(env, deps) {
|
|
16563
|
+
const provided = env.AGENTSHELL_SESSION_ID;
|
|
16564
|
+
if (provided === undefined || provided === "") {
|
|
16565
|
+
return deps.randomUUID();
|
|
16566
|
+
}
|
|
16567
|
+
if (provided.includes("..") || provided.includes("/") || provided.includes("\\") || !SESSION_ID_PATTERN.test(provided)) {
|
|
16568
|
+
deps.writeStderr(`agent-shell: invalid AGENTSHELL_SESSION_ID "${provided}", generating new ID\n`);
|
|
16569
|
+
return deps.randomUUID();
|
|
16570
|
+
}
|
|
16571
|
+
return provided;
|
|
15938
16572
|
}
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
|
|
15950
|
-
|
|
15951
|
-
|
|
15952
|
-
|
|
15953
|
-
|
|
15954
|
-
foldedLines = [];
|
|
16573
|
+
async function resolveWriteEventsDir(env, deps) {
|
|
16574
|
+
const projectRoot = deps.cwd();
|
|
16575
|
+
const defaultDir = (0,external_node_path_namespaceObject.join)(projectRoot, DEFAULT_EVENTS_SUBDIR);
|
|
16576
|
+
const logDir = env.AGENTSHELL_LOG_DIR;
|
|
16577
|
+
if (logDir !== undefined && logDir !== "") {
|
|
16578
|
+
// Canonicalize projectRoot for real-path comparisons; handles symlinked cwd
|
|
16579
|
+
const projectRootReal = await deps.realpath(projectRoot);
|
|
16580
|
+
// Reject external paths (allowing either logical or real project root)
|
|
16581
|
+
const resolvedLogical = (0,external_node_path_namespaceObject.resolve)(projectRoot, logDir);
|
|
16582
|
+
if (!isWithinProjectRoot(resolvedLogical, projectRoot) && !isWithinProjectRoot(resolvedLogical, projectRootReal)) {
|
|
16583
|
+
deps.writeStderr(`agent-shell: AGENTSHELL_LOG_DIR resolves outside project root, using default\n`);
|
|
16584
|
+
await deps.mkdir(defaultDir, {
|
|
16585
|
+
recursive: true
|
|
16586
|
+
});
|
|
16587
|
+
return defaultDir;
|
|
15955
16588
|
}
|
|
15956
|
-
|
|
15957
|
-
|
|
15958
|
-
if (
|
|
15959
|
-
|
|
15960
|
-
|
|
16589
|
+
// Validate existing ancestor realpath before mkdir to prevent symlink escape
|
|
16590
|
+
const ancestorReal = await realpathExistingAncestor(resolvedLogical, deps);
|
|
16591
|
+
if (ancestorReal === null || !isWithinProjectRoot(ancestorReal, projectRootReal)) {
|
|
16592
|
+
deps.writeStderr(`agent-shell: AGENTSHELL_LOG_DIR resolves outside project root via ancestor symlink, using default\n`);
|
|
16593
|
+
await deps.mkdir(defaultDir, {
|
|
16594
|
+
recursive: true
|
|
16595
|
+
});
|
|
16596
|
+
return defaultDir;
|
|
16597
|
+
}
|
|
16598
|
+
await deps.mkdir(resolvedLogical, {
|
|
16599
|
+
recursive: true
|
|
16600
|
+
});
|
|
16601
|
+
const resolved = await deps.realpath(resolvedLogical);
|
|
16602
|
+
if (!isWithinProjectRoot(resolved, projectRootReal)) {
|
|
16603
|
+
deps.writeStderr(`agent-shell: AGENTSHELL_LOG_DIR resolves outside project root, using default\n`);
|
|
16604
|
+
await deps.mkdir(defaultDir, {
|
|
16605
|
+
recursive: true
|
|
16606
|
+
});
|
|
16607
|
+
return defaultDir;
|
|
15961
16608
|
}
|
|
16609
|
+
return resolved;
|
|
15962
16610
|
}
|
|
15963
|
-
|
|
15964
|
-
|
|
15965
|
-
|
|
15966
|
-
|
|
15967
|
-
|
|
15968
|
-
|
|
15969
|
-
|
|
15970
|
-
|
|
15971
|
-
|
|
15972
|
-
|
|
15973
|
-
|
|
15974
|
-
|
|
15975
|
-
|
|
15976
|
-
|
|
15977
|
-
|
|
15978
|
-
|
|
15979
|
-
|
|
15980
|
-
|
|
15981
|
-
|
|
15982
|
-
|
|
15983
|
-
|
|
15984
|
-
|
|
15985
|
-
|
|
15986
|
-
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
|
|
15990
|
-
|
|
15991
|
-
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
}
|
|
16611
|
+
await deps.mkdir(defaultDir, {
|
|
16612
|
+
recursive: true
|
|
16613
|
+
});
|
|
16614
|
+
return defaultDir;
|
|
16615
|
+
}
|
|
16616
|
+
async function writeEvent(eventsDir, sessionId, event, deps) {
|
|
16617
|
+
const filePath = (0,external_node_path_namespaceObject.join)(eventsDir, `${sessionId}.jsonl`);
|
|
16618
|
+
const line = `${JSON.stringify(event)}\n`;
|
|
16619
|
+
await deps.appendFile(filePath, line);
|
|
16620
|
+
}
|
|
16621
|
+
async function emitScriptEndEvent(options, deps) {
|
|
16622
|
+
const sessionId = resolveSessionId(options.env, deps);
|
|
16623
|
+
const eventsDir = await resolveWriteEventsDir(options.env, deps);
|
|
16624
|
+
const capturedEnv = captureEnv(options.env);
|
|
16625
|
+
const tags = captureTags(options.env);
|
|
16626
|
+
const event = {
|
|
16627
|
+
v: SCHEMA_VERSION,
|
|
16628
|
+
session_id: sessionId,
|
|
16629
|
+
event: "script_end",
|
|
16630
|
+
command: options.command,
|
|
16631
|
+
actor: detectActor(options.env),
|
|
16632
|
+
exit_code: options.result.exitCode,
|
|
16633
|
+
signal: options.result.signal,
|
|
16634
|
+
duration_ms: options.result.durationMs,
|
|
16635
|
+
timestamp: deps.now(),
|
|
16636
|
+
env: capturedEnv,
|
|
16637
|
+
tags,
|
|
16638
|
+
...options.env.npm_lifecycle_event && {
|
|
16639
|
+
script: options.env.npm_lifecycle_event
|
|
16640
|
+
},
|
|
16641
|
+
...options.env.npm_package_name && {
|
|
16642
|
+
package: options.env.npm_package_name
|
|
16643
|
+
},
|
|
16644
|
+
...options.env.npm_package_version && {
|
|
16645
|
+
package_version: options.env.npm_package_version
|
|
15999
16646
|
}
|
|
16000
|
-
|
|
16001
|
-
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
|
|
16019
|
-
|
|
16020
|
-
|
|
16021
|
-
|
|
16647
|
+
};
|
|
16648
|
+
await writeEvent(eventsDir, sessionId, event, deps);
|
|
16649
|
+
}
|
|
16650
|
+
async function emitShimErrorEvent(options, deps) {
|
|
16651
|
+
const sessionId = resolveSessionId(options.env, deps);
|
|
16652
|
+
const eventsDir = await resolveWriteEventsDir(options.env, deps);
|
|
16653
|
+
const capturedEnv = captureEnv(options.env);
|
|
16654
|
+
const tags = captureTags(options.env);
|
|
16655
|
+
const event = {
|
|
16656
|
+
v: SCHEMA_VERSION,
|
|
16657
|
+
session_id: sessionId,
|
|
16658
|
+
event: "shim_error",
|
|
16659
|
+
command: options.command,
|
|
16660
|
+
actor: detectActor(options.env),
|
|
16661
|
+
timestamp: deps.now(),
|
|
16662
|
+
env: capturedEnv,
|
|
16663
|
+
tags
|
|
16664
|
+
};
|
|
16665
|
+
await writeEvent(eventsDir, sessionId, event, deps);
|
|
16666
|
+
}
|
|
16667
|
+
async function emitPolicyDecisionEvent(options, deps) {
|
|
16668
|
+
const depsWithProjectRoot = {
|
|
16669
|
+
...deps,
|
|
16670
|
+
cwd: ()=>options.projectRoot
|
|
16671
|
+
};
|
|
16672
|
+
const sessionId = resolveSessionId(options.env, deps);
|
|
16673
|
+
const eventsDir = await resolveWriteEventsDir(options.env, depsWithProjectRoot);
|
|
16674
|
+
const capturedEnv = captureEnv(options.env);
|
|
16675
|
+
const tags = captureTags(options.env);
|
|
16676
|
+
const event = {
|
|
16677
|
+
v: SCHEMA_VERSION,
|
|
16678
|
+
session_id: sessionId,
|
|
16679
|
+
event: "policy_decision",
|
|
16680
|
+
command: options.command,
|
|
16681
|
+
decision: options.decision,
|
|
16682
|
+
matched_rule: options.matched_rule,
|
|
16683
|
+
actor: detectActor(options.env),
|
|
16684
|
+
timestamp: deps.now(),
|
|
16685
|
+
env: capturedEnv,
|
|
16686
|
+
tags
|
|
16687
|
+
};
|
|
16688
|
+
await writeEvent(eventsDir, sessionId, event, depsWithProjectRoot);
|
|
16689
|
+
}
|
|
16690
|
+
async function emitToolUseEvent(options, deps) {
|
|
16691
|
+
const depsWithProjectRoot = {
|
|
16692
|
+
...deps,
|
|
16693
|
+
cwd: ()=>options.projectRoot
|
|
16694
|
+
};
|
|
16695
|
+
const sessionId = resolveSessionId(options.env, deps);
|
|
16696
|
+
const eventsDir = await resolveWriteEventsDir(options.env, depsWithProjectRoot);
|
|
16697
|
+
const capturedEnv = captureEnv(options.env);
|
|
16698
|
+
const tags = captureTags(options.env);
|
|
16699
|
+
const event = {
|
|
16700
|
+
v: SCHEMA_VERSION,
|
|
16701
|
+
session_id: sessionId,
|
|
16702
|
+
event: "tool_use",
|
|
16703
|
+
tool_name: options.tool_name,
|
|
16704
|
+
command: options.command,
|
|
16705
|
+
actor: detectActor(options.env),
|
|
16706
|
+
timestamp: deps.now(),
|
|
16707
|
+
env: capturedEnv,
|
|
16708
|
+
tags
|
|
16709
|
+
};
|
|
16710
|
+
await writeEvent(eventsDir, sessionId, event, depsWithProjectRoot);
|
|
16711
|
+
}
|
|
16712
|
+
|
|
16713
|
+
;// CONCATENATED MODULE: ./src/policy-check.ts
|
|
16714
|
+
// biome-ignore-all lint/style/useNamingConvention: telemetry schema uses snake_case field names
|
|
16715
|
+
|
|
16716
|
+
|
|
16717
|
+
|
|
16718
|
+
|
|
16719
|
+
const TERMINAL_TOOLS = new Set([
|
|
16720
|
+
"bash",
|
|
16721
|
+
"zsh",
|
|
16722
|
+
"ash",
|
|
16723
|
+
"sh"
|
|
16724
|
+
]);
|
|
16725
|
+
const HookInputSchema = schemas_object({
|
|
16726
|
+
toolName: schemas_string(),
|
|
16727
|
+
toolArgs: unknown().optional()
|
|
16728
|
+
});
|
|
16729
|
+
function allowResponse() {
|
|
16730
|
+
return JSON.stringify({
|
|
16731
|
+
permissionDecision: "allow"
|
|
16732
|
+
});
|
|
16733
|
+
}
|
|
16734
|
+
function denyResponse(reason) {
|
|
16735
|
+
return JSON.stringify({
|
|
16736
|
+
permissionDecision: "deny",
|
|
16737
|
+
permissionDecisionReason: reason
|
|
16738
|
+
});
|
|
16739
|
+
}
|
|
16740
|
+
const TELEMETRY_TIMEOUT_MS = 5_000;
|
|
16741
|
+
async function tryEmitTelemetry(deps, command, decision, matchedRule) {
|
|
16742
|
+
try {
|
|
16743
|
+
const repoRoot = deps.policyDeps.getRepositoryRoot();
|
|
16744
|
+
const emission = emitPolicyDecisionEvent({
|
|
16745
|
+
command,
|
|
16746
|
+
decision,
|
|
16747
|
+
matched_rule: matchedRule,
|
|
16748
|
+
env: deps.env,
|
|
16749
|
+
projectRoot: repoRoot
|
|
16750
|
+
}, deps.telemetryDeps);
|
|
16751
|
+
// Prevent unhandled rejection if emission rejects after timeout wins the race
|
|
16752
|
+
emission.catch(()=>{});
|
|
16753
|
+
let timeoutHandle;
|
|
16754
|
+
const timeout = new Promise((_, reject)=>{
|
|
16755
|
+
timeoutHandle = setTimeout(()=>reject(new Error("telemetry write timed out")), TELEMETRY_TIMEOUT_MS);
|
|
16756
|
+
timeoutHandle.unref();
|
|
16757
|
+
});
|
|
16758
|
+
try {
|
|
16759
|
+
await Promise.race([
|
|
16760
|
+
emission,
|
|
16761
|
+
timeout
|
|
16762
|
+
]);
|
|
16763
|
+
} finally{
|
|
16764
|
+
if (timeoutHandle !== undefined) {
|
|
16765
|
+
clearTimeout(timeoutHandle);
|
|
16022
16766
|
}
|
|
16023
16767
|
}
|
|
16768
|
+
} catch (err) {
|
|
16769
|
+
deps.writeStderr(`agent-shell: telemetry write error: ${sanitizeForStderr(err)}\n`);
|
|
16024
16770
|
}
|
|
16025
|
-
flushContinuation();
|
|
16026
|
-
if (isFoldedBlock) {
|
|
16027
|
-
flushFoldedBlock();
|
|
16028
|
-
}
|
|
16029
|
-
return commands;
|
|
16030
16771
|
}
|
|
16031
|
-
|
|
16032
|
-
* Parse mise.toml to extract task definitions.
|
|
16033
|
-
* Uses simple line-based parsing for [tasks.*] sections.
|
|
16034
|
-
*/ async function discoverMiseTasks(targetDir) {
|
|
16035
|
-
const miseTomlPath = (0,external_node_path_namespaceObject.join)(targetDir, "mise.toml");
|
|
16036
|
-
if (!await fileExists(miseTomlPath)) {
|
|
16037
|
-
return [];
|
|
16038
|
-
}
|
|
16772
|
+
async function handlePolicyCheck(deps) {
|
|
16039
16773
|
try {
|
|
16040
|
-
const
|
|
16041
|
-
|
|
16042
|
-
|
|
16774
|
+
const rawStdin = await deps.readStdin();
|
|
16775
|
+
let input;
|
|
16776
|
+
try {
|
|
16777
|
+
input = JSON.parse(rawStdin);
|
|
16778
|
+
} catch {
|
|
16779
|
+
deps.writeStderr("agent-shell: failed to parse stdin as JSON\n");
|
|
16780
|
+
deps.writeStdout(denyResponse("Invalid JSON input"));
|
|
16781
|
+
return;
|
|
16043
16782
|
}
|
|
16044
|
-
const
|
|
16045
|
-
|
|
16046
|
-
|
|
16047
|
-
|
|
16048
|
-
}
|
|
16049
|
-
}
|
|
16050
|
-
function parseMiseTomlTasks(content) {
|
|
16051
|
-
const tasks = [];
|
|
16052
|
-
const lines = content.split("\n");
|
|
16053
|
-
let currentTaskName = null;
|
|
16054
|
-
let inMultiLineRun = false;
|
|
16055
|
-
let multiLineCommand = "";
|
|
16056
|
-
for (const line of lines){
|
|
16057
|
-
const trimmed = line.trim();
|
|
16058
|
-
if (inMultiLineRun) {
|
|
16059
|
-
if (trimmed === '"""' || trimmed === "'''") {
|
|
16060
|
-
inMultiLineRun = false;
|
|
16061
|
-
const cmd = multiLineCommand.trim();
|
|
16062
|
-
if (currentTaskName !== null && cmd.length > 0) {
|
|
16063
|
-
const firstLine = cmd.split("\n").map((l)=>l.trim()).find((l)=>l.length > 0);
|
|
16064
|
-
if (firstLine) {
|
|
16065
|
-
tasks.push({
|
|
16066
|
-
name: currentTaskName,
|
|
16067
|
-
command: firstLine
|
|
16068
|
-
});
|
|
16069
|
-
}
|
|
16070
|
-
}
|
|
16071
|
-
multiLineCommand = "";
|
|
16072
|
-
continue;
|
|
16073
|
-
}
|
|
16074
|
-
multiLineCommand += `${trimmed}\n`;
|
|
16075
|
-
continue;
|
|
16783
|
+
const hookResult = HookInputSchema.safeParse(input);
|
|
16784
|
+
if (!hookResult.success) {
|
|
16785
|
+
deps.writeStdout(denyResponse("Missing or invalid toolName field"));
|
|
16786
|
+
return;
|
|
16076
16787
|
}
|
|
16077
|
-
const
|
|
16078
|
-
|
|
16079
|
-
|
|
16080
|
-
|
|
16788
|
+
const { toolName, toolArgs } = hookResult.data;
|
|
16789
|
+
// Step 3 (per spec): load and validate policy before terminal tool check
|
|
16790
|
+
// (fail-closed on invalid policy, even for non-terminal tools)
|
|
16791
|
+
let policy;
|
|
16792
|
+
try {
|
|
16793
|
+
policy = await loadPolicy(deps.env, deps.policyDeps);
|
|
16794
|
+
} catch (err) {
|
|
16795
|
+
deps.writeStderr(`agent-shell: policy load error: ${sanitizeForStderr(err)}\n`);
|
|
16796
|
+
deps.writeStdout(denyResponse("Failed to load policy"));
|
|
16797
|
+
return;
|
|
16081
16798
|
}
|
|
16082
|
-
|
|
16083
|
-
|
|
16084
|
-
|
|
16799
|
+
// Step 4 (per spec): non-terminal tools pass through with allow.
|
|
16800
|
+
// Per spec: command field is empty string for non-terminal tool decisions
|
|
16801
|
+
// (toolArgs is never parsed, so no command string is available).
|
|
16802
|
+
if (!TERMINAL_TOOLS.has(toolName)) {
|
|
16803
|
+
deps.writeStdout(allowResponse());
|
|
16804
|
+
await tryEmitTelemetry(deps, "", "allow", null);
|
|
16805
|
+
return;
|
|
16085
16806
|
}
|
|
16086
|
-
|
|
16087
|
-
|
|
16088
|
-
|
|
16089
|
-
|
|
16090
|
-
continue;
|
|
16091
|
-
}
|
|
16092
|
-
const runMatch = trimmed.match(/^run\s*=\s*(?:"([^"]*)"|'([^']*)')/);
|
|
16093
|
-
if (runMatch) {
|
|
16094
|
-
const command = runMatch[1] ?? runMatch[2] ?? "";
|
|
16095
|
-
if (command.length > 0) {
|
|
16096
|
-
tasks.push({
|
|
16097
|
-
name: currentTaskName,
|
|
16098
|
-
command
|
|
16099
|
-
});
|
|
16100
|
-
}
|
|
16101
|
-
}
|
|
16807
|
+
// Terminal tool — validate toolArgs
|
|
16808
|
+
if (typeof toolArgs !== "string") {
|
|
16809
|
+
deps.writeStdout(denyResponse("Missing or non-string toolArgs for terminal tool"));
|
|
16810
|
+
return;
|
|
16102
16811
|
}
|
|
16103
|
-
|
|
16104
|
-
|
|
16105
|
-
|
|
16106
|
-
|
|
16107
|
-
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
detected.add(language);
|
|
16812
|
+
let parsedArgs;
|
|
16813
|
+
try {
|
|
16814
|
+
parsedArgs = JSON.parse(toolArgs);
|
|
16815
|
+
} catch {
|
|
16816
|
+
deps.writeStderr("agent-shell: failed to parse toolArgs as JSON\n");
|
|
16817
|
+
deps.writeStdout(denyResponse("Invalid JSON in toolArgs"));
|
|
16818
|
+
return;
|
|
16111
16819
|
}
|
|
16112
|
-
|
|
16113
|
-
|
|
16114
|
-
|
|
16115
|
-
|
|
16116
|
-
}
|
|
16117
|
-
|
|
16118
|
-
|
|
16119
|
-
|
|
16120
|
-
|
|
16121
|
-
|
|
16122
|
-
|
|
16123
|
-
|
|
16124
|
-
|
|
16125
|
-
|
|
16126
|
-
|
|
16127
|
-
|
|
16128
|
-
|
|
16129
|
-
|
|
16130
|
-
|
|
16131
|
-
|
|
16132
|
-
|
|
16820
|
+
// toolArgs must be a non-null plain object
|
|
16821
|
+
if (parsedArgs === null || typeof parsedArgs !== "object" || Array.isArray(parsedArgs)) {
|
|
16822
|
+
deps.writeStdout(denyResponse("toolArgs must be a non-null plain object for terminal tools"));
|
|
16823
|
+
return;
|
|
16824
|
+
}
|
|
16825
|
+
const obj = parsedArgs;
|
|
16826
|
+
if (!Object.hasOwn(obj, "command")) {
|
|
16827
|
+
deps.writeStdout(denyResponse("Missing command field in toolArgs"));
|
|
16828
|
+
return;
|
|
16829
|
+
}
|
|
16830
|
+
if (typeof obj.command !== "string") {
|
|
16831
|
+
deps.writeStdout(denyResponse("command field must be a string"));
|
|
16832
|
+
return;
|
|
16833
|
+
}
|
|
16834
|
+
const command = obj.command;
|
|
16835
|
+
const result = evaluatePolicy(policy, command);
|
|
16836
|
+
const responseJson = result.decision === "allow" ? allowResponse() : denyResponse(`Command '${command}' denied by policy rule: ${result.matchedRule ?? "not in allow list"}`);
|
|
16837
|
+
deps.writeStdout(responseJson);
|
|
16838
|
+
await tryEmitTelemetry(deps, command.trim(), result.decision, result.matchedRule);
|
|
16839
|
+
} catch (err) {
|
|
16840
|
+
deps.writeStderr(`agent-shell: unexpected error: ${sanitizeForStderr(err)}\n`);
|
|
16841
|
+
deps.writeStdout(denyResponse("Internal error during policy evaluation"));
|
|
16842
|
+
}
|
|
16133
16843
|
}
|
|
16134
16844
|
|
|
16135
|
-
;// CONCATENATED MODULE: ./src/
|
|
16136
|
-
|
|
16137
|
-
|
|
16845
|
+
;// CONCATENATED MODULE: ./src/record.ts
|
|
16846
|
+
// biome-ignore-all lint/style/useNamingConvention: telemetry schema uses snake_case field names
|
|
16138
16847
|
|
|
16139
16848
|
|
|
16140
16849
|
|
|
16141
|
-
const
|
|
16142
|
-
"
|
|
16143
|
-
"
|
|
16144
|
-
"
|
|
16145
|
-
"
|
|
16146
|
-
|
|
16147
|
-
|
|
16148
|
-
|
|
16149
|
-
|
|
16150
|
-
|
|
16151
|
-
|
|
16152
|
-
"rm -rf *",
|
|
16153
|
-
"sudo *"
|
|
16154
|
-
];
|
|
16155
|
-
const POLICY_SUBPATH = ".github/hooks/agent-shell/policy.json";
|
|
16156
|
-
const HOOKS_SUBPATH = ".github/hooks/agent-shell/hooks.json";
|
|
16157
|
-
/**
|
|
16158
|
-
* Extracts the script or task name from a command string, skipping any
|
|
16159
|
-
* flags (tokens starting with `-`) that appear between the prefix and
|
|
16160
|
-
* the actual name. For example, `npm run -s build` or `npm run --silent build`
|
|
16161
|
-
* both return `build`. Splits on any whitespace to avoid empty tokens from
|
|
16162
|
-
* multiple consecutive spaces.
|
|
16163
|
-
*/ function extractNameAfterPrefix(cmd, prefix) {
|
|
16164
|
-
const tokens = cmd.slice(prefix.length).trim().split(/\s+/);
|
|
16165
|
-
for (const token of tokens){
|
|
16166
|
-
if (token.length > 0 && !token.startsWith("-")) {
|
|
16167
|
-
return token;
|
|
16168
|
-
}
|
|
16169
|
-
}
|
|
16170
|
-
return "";
|
|
16171
|
-
}
|
|
16850
|
+
const record_TERMINAL_TOOLS = new Set([
|
|
16851
|
+
"bash",
|
|
16852
|
+
"zsh",
|
|
16853
|
+
"ash",
|
|
16854
|
+
"sh"
|
|
16855
|
+
]);
|
|
16856
|
+
const MAX_COMMAND_BYTES = 4096;
|
|
16857
|
+
const record_HookInputSchema = schemas_object({
|
|
16858
|
+
toolName: schemas_string().max(1024),
|
|
16859
|
+
toolArgs: unknown().optional()
|
|
16860
|
+
});
|
|
16172
16861
|
/**
|
|
16173
|
-
*
|
|
16174
|
-
* Creates an allow list of commands discovered in the project,
|
|
16175
|
-
* plus common safe defaults. Includes standard deny rules.
|
|
16862
|
+
* Extracts a command string from toolArgs for terminal tools.
|
|
16176
16863
|
*
|
|
16177
|
-
*
|
|
16178
|
-
*
|
|
16179
|
-
*
|
|
16180
|
-
*
|
|
16181
|
-
*
|
|
16182
|
-
|
|
16183
|
-
|
|
16184
|
-
|
|
16185
|
-
|
|
16864
|
+
* Parsing chain (mirrors policy-check.ts but observation-only):
|
|
16865
|
+
* 1. Check if toolArgs is a string
|
|
16866
|
+
* 2. Parse that string as JSON
|
|
16867
|
+
* 3. Verify result is a non-null plain object
|
|
16868
|
+
* 4. Check for a `command` property
|
|
16869
|
+
* 5. Verify `command` is a string
|
|
16870
|
+
*
|
|
16871
|
+
* Returns the command string on success, or an empty string for any failure.
|
|
16872
|
+
*/ function extractTerminalCommand(toolArgs) {
|
|
16873
|
+
// Step 1: toolArgs must be a string
|
|
16874
|
+
if (typeof toolArgs !== "string") {
|
|
16875
|
+
return "";
|
|
16186
16876
|
}
|
|
16187
|
-
|
|
16188
|
-
|
|
16189
|
-
|
|
16190
|
-
|
|
16191
|
-
|
|
16192
|
-
|
|
16193
|
-
continue;
|
|
16194
|
-
}
|
|
16195
|
-
if (name === "test") {
|
|
16196
|
-
allowSet.add("npm test");
|
|
16197
|
-
} else {
|
|
16198
|
-
allowSet.add(`npm run ${name}`);
|
|
16199
|
-
}
|
|
16877
|
+
// Step 2: parse as JSON
|
|
16878
|
+
let parsedArgs;
|
|
16879
|
+
try {
|
|
16880
|
+
parsedArgs = JSON.parse(toolArgs);
|
|
16881
|
+
} catch {
|
|
16882
|
+
return "";
|
|
16200
16883
|
}
|
|
16201
|
-
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
if (cmd !== "npm test" && !SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
16205
|
-
allowSet.add(cmd);
|
|
16206
|
-
}
|
|
16207
|
-
} else if (cmd.startsWith("npm run ")) {
|
|
16208
|
-
const scriptName = extractNameAfterPrefix(cmd, "npm run ");
|
|
16209
|
-
if (scriptName) {
|
|
16210
|
-
allowSet.add(`npm run ${scriptName}`);
|
|
16211
|
-
if (cmd !== `npm run ${scriptName}` && !SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
16212
|
-
allowSet.add(cmd);
|
|
16213
|
-
}
|
|
16214
|
-
} else if (!SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
16215
|
-
allowSet.add(cmd);
|
|
16216
|
-
}
|
|
16217
|
-
} else if (cmd === "npm ci" || cmd === "npm install") {
|
|
16218
|
-
allowSet.add(cmd);
|
|
16219
|
-
} else if (cmd.startsWith("npx ")) {
|
|
16220
|
-
if (!SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
16221
|
-
allowSet.add(cmd);
|
|
16222
|
-
}
|
|
16223
|
-
} else if (cmd.startsWith("mise run ")) {
|
|
16224
|
-
const taskName = extractNameAfterPrefix(cmd, "mise run ");
|
|
16225
|
-
if (taskName) {
|
|
16226
|
-
allowSet.add(`mise run ${taskName}`);
|
|
16227
|
-
if (cmd !== `mise run ${taskName}` && !SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
16228
|
-
allowSet.add(cmd);
|
|
16229
|
-
}
|
|
16230
|
-
} else if (!SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
16231
|
-
allowSet.add(cmd);
|
|
16232
|
-
}
|
|
16233
|
-
} else {
|
|
16234
|
-
if (!SHELL_METACHAR_PATTERN.test(cmd)) {
|
|
16235
|
-
allowSet.add(cmd);
|
|
16236
|
-
}
|
|
16237
|
-
}
|
|
16884
|
+
// Step 3: must be a non-null plain object
|
|
16885
|
+
if (parsedArgs === null || typeof parsedArgs !== "object" || Array.isArray(parsedArgs)) {
|
|
16886
|
+
return "";
|
|
16238
16887
|
}
|
|
16239
|
-
|
|
16240
|
-
|
|
16241
|
-
|
|
16242
|
-
|
|
16243
|
-
}
|
|
16888
|
+
// Step 4: must have `command` property (reject prototype pollution keys)
|
|
16889
|
+
const obj = parsedArgs;
|
|
16890
|
+
if (Object.hasOwn(obj, "__proto__") || Object.hasOwn(obj, "constructor") || Object.hasOwn(obj, "prototype")) {
|
|
16891
|
+
return "";
|
|
16244
16892
|
}
|
|
16245
|
-
if (
|
|
16246
|
-
|
|
16893
|
+
if (!Object.hasOwn(obj, "command")) {
|
|
16894
|
+
return "";
|
|
16247
16895
|
}
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
|
|
16257
|
-
|
|
16258
|
-
|
|
16259
|
-
|
|
16260
|
-
|
|
16261
|
-
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
|
|
16269
|
-
|
|
16270
|
-
|
|
16271
|
-
]
|
|
16272
|
-
}
|
|
16273
|
-
};
|
|
16274
|
-
}
|
|
16275
|
-
/**
|
|
16276
|
-
* Writes a file atomically, skipping if the file already exists.
|
|
16277
|
-
* Uses the `wx` (exclusive create) flag to avoid TOCTOU races
|
|
16278
|
-
* between a check and write.
|
|
16279
|
-
*/ async function writeFileIfNotExists(filePath, parentDir, content, subpath, writeStdout) {
|
|
16280
|
-
await (0,promises_namespaceObject.mkdir)(parentDir, {
|
|
16281
|
-
recursive: true
|
|
16282
|
-
});
|
|
16896
|
+
// Step 5: command must be a string
|
|
16897
|
+
if (typeof obj.command !== "string") {
|
|
16898
|
+
return "";
|
|
16899
|
+
}
|
|
16900
|
+
// Truncate to MAX_COMMAND_BYTES using byte-aware slicing (matching env-capture.ts pattern)
|
|
16901
|
+
if (Buffer.byteLength(obj.command, "utf-8") > MAX_COMMAND_BYTES) {
|
|
16902
|
+
// Codepoint-aware truncation: stop before exceeding the byte limit.
|
|
16903
|
+
// for...of iterates Unicode codepoints, so surrogate pairs are handled
|
|
16904
|
+
// correctly and we never truncate mid-sequence.
|
|
16905
|
+
let byteCount = 0;
|
|
16906
|
+
let truncated = "";
|
|
16907
|
+
for (const ch of obj.command){
|
|
16908
|
+
const charBytes = Buffer.byteLength(ch, "utf-8");
|
|
16909
|
+
if (byteCount + charBytes > MAX_COMMAND_BYTES) break;
|
|
16910
|
+
byteCount += charBytes;
|
|
16911
|
+
truncated += ch;
|
|
16912
|
+
}
|
|
16913
|
+
return truncated;
|
|
16914
|
+
}
|
|
16915
|
+
return obj.command;
|
|
16916
|
+
}
|
|
16917
|
+
async function handleRecord(deps) {
|
|
16918
|
+
let rawStdin;
|
|
16283
16919
|
try {
|
|
16284
|
-
await (
|
|
16285
|
-
|
|
16286
|
-
});
|
|
16287
|
-
|
|
16288
|
-
} catch (error) {
|
|
16289
|
-
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
16290
|
-
writeStdout(`Skipping ${subpath} — file already exists\n`);
|
|
16291
|
-
} else {
|
|
16292
|
-
throw error;
|
|
16293
|
-
}
|
|
16920
|
+
rawStdin = await deps.readStdin();
|
|
16921
|
+
} catch (err) {
|
|
16922
|
+
deps.writeStderr(`agent-shell: failed to read stdin: ${sanitizeForStderr(err)}\n`);
|
|
16923
|
+
return false;
|
|
16294
16924
|
}
|
|
16295
|
-
|
|
16296
|
-
|
|
16297
|
-
|
|
16298
|
-
|
|
16299
|
-
|
|
16300
|
-
|
|
16301
|
-
*/ async function handlePolicyInit(deps) {
|
|
16302
|
-
const repoRoot = deps.getRepositoryRoot();
|
|
16303
|
-
deps.writeStdout("Scanning project...\n");
|
|
16304
|
-
const scanResult = await scanProject(repoRoot);
|
|
16305
|
-
deps.writeStdout(`Discovered: ${scanResult.scripts.length} npm script(s), ` + `${scanResult.workflowCommands.length} workflow command(s), ` + `${scanResult.miseTasks.length} mise task(s), ` + `${scanResult.languages.length} language(s)\n`);
|
|
16306
|
-
const policy = generatePolicy(scanResult);
|
|
16307
|
-
const enhanced = await enhanceWithCopilot(scanResult, repoRoot, deps.writeStderr, deps.model);
|
|
16308
|
-
if (enhanced !== null) {
|
|
16309
|
-
deps.writeStdout("Enhanced with Copilot analysis\n");
|
|
16310
|
-
if (enhanced.additionalAllowRules.length > 0) {
|
|
16311
|
-
deps.writeStdout("\nSuggested additional allow rules from Copilot (not auto-applied):\n");
|
|
16312
|
-
for (const rule of enhanced.additionalAllowRules){
|
|
16313
|
-
if (isSafeCommand(rule)) {
|
|
16314
|
-
deps.writeStdout(` - ${sanitizeOutput(rule)}\n`);
|
|
16315
|
-
} else {
|
|
16316
|
-
deps.writeStdout(` - [UNSAFE, skipped] ${sanitizeOutput(rule)}\n`);
|
|
16317
|
-
}
|
|
16318
|
-
}
|
|
16319
|
-
}
|
|
16320
|
-
if (enhanced.suggestions.length > 0) {
|
|
16321
|
-
deps.writeStdout("\nSuggestions from Copilot:\n");
|
|
16322
|
-
for (const suggestion of enhanced.suggestions){
|
|
16323
|
-
deps.writeStdout(` - ${sanitizeOutput(suggestion)}\n`);
|
|
16324
|
-
}
|
|
16325
|
-
}
|
|
16925
|
+
let input;
|
|
16926
|
+
try {
|
|
16927
|
+
input = JSON.parse(rawStdin);
|
|
16928
|
+
} catch {
|
|
16929
|
+
deps.writeStderr("agent-shell: failed to parse stdin as JSON\n");
|
|
16930
|
+
return false;
|
|
16326
16931
|
}
|
|
16327
|
-
const
|
|
16328
|
-
|
|
16329
|
-
|
|
16330
|
-
|
|
16331
|
-
|
|
16332
|
-
|
|
16333
|
-
|
|
16334
|
-
|
|
16335
|
-
|
|
16336
|
-
|
|
16337
|
-
|
|
16932
|
+
const hookResult = record_HookInputSchema.safeParse(input);
|
|
16933
|
+
if (!hookResult.success) {
|
|
16934
|
+
deps.writeStderr("agent-shell: missing or invalid toolName field, skipping telemetry\n");
|
|
16935
|
+
return false;
|
|
16936
|
+
}
|
|
16937
|
+
const { toolName, toolArgs } = hookResult.data;
|
|
16938
|
+
const command = record_TERMINAL_TOOLS.has(toolName) ? extractTerminalCommand(toolArgs) : "";
|
|
16939
|
+
try {
|
|
16940
|
+
const repoRoot = deps.getRepositoryRoot();
|
|
16941
|
+
await emitToolUseEvent({
|
|
16942
|
+
tool_name: toolName,
|
|
16943
|
+
command,
|
|
16944
|
+
env: deps.env,
|
|
16945
|
+
projectRoot: repoRoot
|
|
16946
|
+
}, deps.telemetryDeps);
|
|
16947
|
+
} catch (err) {
|
|
16948
|
+
deps.writeStderr(`agent-shell: telemetry write error: ${sanitizeForStderr(err)}\n`);
|
|
16949
|
+
return false;
|
|
16950
|
+
}
|
|
16951
|
+
return true;
|
|
16338
16952
|
}
|
|
16339
16953
|
|
|
16340
16954
|
;// CONCATENATED MODULE: ./src/shim.ts
|
|
@@ -16428,12 +17042,18 @@ async function runShim(options) {
|
|
|
16428
17042
|
|
|
16429
17043
|
|
|
16430
17044
|
|
|
17045
|
+
|
|
17046
|
+
|
|
17047
|
+
|
|
17048
|
+
|
|
16431
17049
|
const VERSION = "0.1.0";
|
|
16432
17050
|
const USAGE = `Usage: agent-shell -c <command>
|
|
17051
|
+
agent-shell init [--flight-recorder] [--policy] [--no-flight-recorder] [--no-policy]
|
|
17052
|
+
agent-shell record
|
|
16433
17053
|
agent-shell policy-check
|
|
16434
17054
|
agent-shell policy --init [--model=<model>]
|
|
16435
|
-
agent-shell --version
|
|
16436
17055
|
agent-shell log
|
|
17056
|
+
agent-shell --version
|
|
16437
17057
|
|
|
16438
17058
|
Environment:
|
|
16439
17059
|
AGENTSHELL_PASSTHROUGH=1 Bypass instrumentation
|
|
@@ -16496,6 +17116,79 @@ async function main() {
|
|
|
16496
17116
|
}
|
|
16497
17117
|
return;
|
|
16498
17118
|
}
|
|
17119
|
+
case "record":
|
|
17120
|
+
{
|
|
17121
|
+
const getRepositoryRoot = createGetRepositoryRoot(undefined, process.env);
|
|
17122
|
+
try {
|
|
17123
|
+
await handleRecord({
|
|
17124
|
+
readStdin: ()=>readStdin(),
|
|
17125
|
+
writeStderr: (data)=>process.stderr.write(data),
|
|
17126
|
+
env: process.env,
|
|
17127
|
+
telemetryDeps: createDefaultDeps(),
|
|
17128
|
+
getRepositoryRoot
|
|
17129
|
+
});
|
|
17130
|
+
} catch (err) {
|
|
17131
|
+
// Log unexpected exceptions but don't change exit code
|
|
17132
|
+
process.stderr.write(`agent-shell: record error: ${sanitizeForStderr(err)}\n`);
|
|
17133
|
+
}
|
|
17134
|
+
// Observation-only: always exit 0 regardless of parse/emit errors.
|
|
17135
|
+
// postToolUse hooks must not block agent operations and the agent
|
|
17136
|
+
// ignores the exit code anyway.
|
|
17137
|
+
process.exitCode = 0;
|
|
17138
|
+
return;
|
|
17139
|
+
}
|
|
17140
|
+
case "init":
|
|
17141
|
+
{
|
|
17142
|
+
try {
|
|
17143
|
+
if (mode.unknownArgs.length > 0) {
|
|
17144
|
+
process.stderr.write(`agent-shell: unknown flag(s): ${mode.unknownArgs.map(sanitizeForStderr).join(", ")}\n`);
|
|
17145
|
+
process.exitCode = 1;
|
|
17146
|
+
return;
|
|
17147
|
+
}
|
|
17148
|
+
const getRepositoryRoot = createGetRepositoryRoot(undefined, process.env);
|
|
17149
|
+
const isTty = Boolean(process.stdin.isTTY);
|
|
17150
|
+
const prompt = isTty ? async (message)=>{
|
|
17151
|
+
const rl = (0,external_node_readline_namespaceObject.createInterface)({
|
|
17152
|
+
input: process.stdin,
|
|
17153
|
+
output: process.stdout
|
|
17154
|
+
});
|
|
17155
|
+
try {
|
|
17156
|
+
return await new Promise((resolvePrompt)=>{
|
|
17157
|
+
rl.question(`${message} (Y/n) `, (answer)=>{
|
|
17158
|
+
const trimmed = answer.trim().toLowerCase();
|
|
17159
|
+
resolvePrompt(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
17160
|
+
});
|
|
17161
|
+
});
|
|
17162
|
+
} finally{
|
|
17163
|
+
rl.close();
|
|
17164
|
+
}
|
|
17165
|
+
} : undefined;
|
|
17166
|
+
const ok = await handleInit({
|
|
17167
|
+
flightRecorder: mode.flightRecorder,
|
|
17168
|
+
policy: mode.policy,
|
|
17169
|
+
noFlightRecorder: mode.noFlightRecorder,
|
|
17170
|
+
noPolicy: mode.noPolicy
|
|
17171
|
+
}, {
|
|
17172
|
+
getRepositoryRoot,
|
|
17173
|
+
writeStdout: (data)=>process.stdout.write(data),
|
|
17174
|
+
writeStderr: (data)=>process.stderr.write(data),
|
|
17175
|
+
readFile: (path, encoding)=>(0,promises_namespaceObject.readFile)(path, encoding),
|
|
17176
|
+
writeFile: (path, content, options)=>(0,promises_namespaceObject.writeFile)(path, content, options),
|
|
17177
|
+
rename: (oldPath, newPath)=>(0,promises_namespaceObject.rename)(oldPath, newPath),
|
|
17178
|
+
unlink: (path)=>(0,promises_namespaceObject.unlink)(path),
|
|
17179
|
+
mkdir: (path, opts)=>(0,promises_namespaceObject.mkdir)(path, opts).then(()=>undefined),
|
|
17180
|
+
realpath: (path)=>(0,promises_namespaceObject.realpath)(path),
|
|
17181
|
+
scanProject: (dir)=>scanProject(dir),
|
|
17182
|
+
isTty,
|
|
17183
|
+
prompt
|
|
17184
|
+
});
|
|
17185
|
+
process.exitCode = ok ? 0 : 1;
|
|
17186
|
+
} catch (err) {
|
|
17187
|
+
process.stderr.write(`agent-shell: init error: ${sanitizeForStderr(err)}\n`);
|
|
17188
|
+
process.exitCode = 1;
|
|
17189
|
+
}
|
|
17190
|
+
return;
|
|
17191
|
+
}
|
|
16499
17192
|
case "passthrough":
|
|
16500
17193
|
{
|
|
16501
17194
|
const result = (0,external_node_child_process_namespaceObject.spawnSync)("/bin/sh", mode.args, {
|