@pugi/cli 0.1.0-beta.99 → 1.0.0-alpha.10
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/LICENSE +1 -1
- package/README.md +11 -191
- package/bin/pugi +8 -0
- package/package.json +15 -71
- package/postinstall.mjs +31 -0
- package/CHANGELOG.md +0 -132
- package/THIRD_PARTY_NOTICES.md +0 -40
- package/assets/pugi-mascot.ansi +0 -16
- package/assets/pugi-prozr2-mascot.ansi +0 -9
- package/bin/run.js +0 -34
- package/dist/commands/deploy.js +0 -439
- package/dist/commands/flatten.js +0 -191
- package/dist/commands/jobs-watch.js +0 -201
- package/dist/commands/jobs.js +0 -260
- package/dist/commands/retro.js +0 -210
- package/dist/commands/smoke.js +0 -133
- package/dist/core/agent-progress/cleanup.js +0 -134
- package/dist/core/agent-progress/schema.js +0 -144
- package/dist/core/agent-progress/writer.js +0 -101
- package/dist/core/agents/adaptive-router.js +0 -330
- package/dist/core/agents/loader.js +0 -104
- package/dist/core/agents/query-decomposer.js +0 -297
- package/dist/core/agents/registry.js +0 -69
- package/dist/core/approvals/shortcut-resolver.js +0 -98
- package/dist/core/artifact-chain/dispatcher.js +0 -148
- package/dist/core/artifact-chain/exporter.js +0 -164
- package/dist/core/artifact-chain/state.js +0 -243
- package/dist/core/artifact-chain/steps.js +0 -169
- package/dist/core/ask-user/question.js +0 -92
- package/dist/core/audit/audit-trail.js +0 -275
- package/dist/core/auth/ensure-authenticated.js +0 -129
- package/dist/core/auth/env-provider.js +0 -238
- package/dist/core/auto-open-browser.js +0 -128
- package/dist/core/auto-update/channels.js +0 -122
- package/dist/core/auto-update/checker.js +0 -241
- package/dist/core/auto-update/state.js +0 -235
- package/dist/core/bare-mode/index.js +0 -107
- package/dist/core/bash/redirect.js +0 -281
- package/dist/core/bash-classifier.js +0 -1397
- package/dist/core/checkpoint/resumer.js +0 -149
- package/dist/core/checkpoint/rewinder.js +0 -291
- package/dist/core/checkpoints/shadow-git.js +0 -670
- package/dist/core/citations/parser.js +0 -109
- package/dist/core/classifier/yolo-classifier.js +0 -88
- package/dist/core/clipboard.js +0 -70
- package/dist/core/codegraph/decision-store.js +0 -248
- package/dist/core/codegraph/detect-repo.js +0 -459
- package/dist/core/codegraph/install.js +0 -134
- package/dist/core/codegraph/offer-hook.js +0 -220
- package/dist/core/compact/auto-trigger.js +0 -96
- package/dist/core/compact/buffer-rewriter.js +0 -115
- package/dist/core/compact/summarizer.js +0 -208
- package/dist/core/compact/token-counter.js +0 -108
- package/dist/core/consensus/anvil-fanout.js +0 -276
- package/dist/core/consensus/diff-capture.js +0 -491
- package/dist/core/consensus/rubric.js +0 -233
- package/dist/core/context/builder.js +0 -114
- package/dist/core/context/compaction-events.js +0 -99
- package/dist/core/context/compaction.js +0 -602
- package/dist/core/context/index.js +0 -28
- package/dist/core/context/invariants.js +0 -250
- package/dist/core/context/markdown-loader.js +0 -288
- package/dist/core/context/markdown-traverse.js +0 -255
- package/dist/core/context/pugiignore.js +0 -316
- package/dist/core/context/repo-skeleton.js +0 -533
- package/dist/core/context/tool-eviction.js +0 -55
- package/dist/core/context/watcher.js +0 -342
- package/dist/core/context/working-set.js +0 -165
- package/dist/core/coordinator/agent-tools.js +0 -77
- package/dist/core/coordinator/agent-toolset.js +0 -65
- package/dist/core/coordinator/fsm.js +0 -73
- package/dist/core/coordinator/mode-fsm.js +0 -70
- package/dist/core/cost/rate-card.js +0 -129
- package/dist/core/cost/tracker.js +0 -221
- package/dist/core/credentials.js +0 -355
- package/dist/core/cron/scheduler.js +0 -138
- package/dist/core/denial-tracking/index.js +0 -8
- package/dist/core/denial-tracking/state.js +0 -264
- package/dist/core/diagnostics/probe-runner.js +0 -93
- package/dist/core/diagnostics/probes/api.js +0 -46
- package/dist/core/diagnostics/probes/auth.js +0 -93
- package/dist/core/diagnostics/probes/bare-mode.js +0 -42
- package/dist/core/diagnostics/probes/cli-version.js +0 -127
- package/dist/core/diagnostics/probes/config.js +0 -72
- package/dist/core/diagnostics/probes/denial-tracking.js +0 -57
- package/dist/core/diagnostics/probes/disk.js +0 -81
- package/dist/core/diagnostics/probes/engine-live.js +0 -46
- package/dist/core/diagnostics/probes/git.js +0 -65
- package/dist/core/diagnostics/probes/hooks.js +0 -118
- package/dist/core/diagnostics/probes/mcp.js +0 -75
- package/dist/core/diagnostics/probes/node.js +0 -59
- package/dist/core/diagnostics/probes/pnpm.js +0 -36
- package/dist/core/diagnostics/probes/pugi-md.js +0 -89
- package/dist/core/diagnostics/probes/sandbox.js +0 -72
- package/dist/core/diagnostics/probes/session.js +0 -74
- package/dist/core/diagnostics/probes/status-snapshot.js +0 -488
- package/dist/core/diagnostics/probes/workspace.js +0 -63
- package/dist/core/diagnostics/types.js +0 -70
- package/dist/core/dispatch/cache-cleanup.js +0 -197
- package/dist/core/dispatch/cache-handoff.js +0 -295
- package/dist/core/edits/apply-patch-layer-e.js +0 -189
- package/dist/core/edits/dispatch.js +0 -511
- package/dist/core/edits/format-detector.js +0 -260
- package/dist/core/edits/format-matrix.js +0 -26
- package/dist/core/edits/fuzzy-ladder.js +0 -650
- package/dist/core/edits/index.js +0 -19
- package/dist/core/edits/journal.js +0 -199
- package/dist/core/edits/layer-a-apply.js +0 -217
- package/dist/core/edits/layer-a-fuzzy-apply.js +0 -198
- package/dist/core/edits/layer-b-apply.js +0 -211
- package/dist/core/edits/layer-c-apply.js +0 -160
- package/dist/core/edits/layer-d-ast.js +0 -572
- package/dist/core/edits/marker-parser.js +0 -401
- package/dist/core/edits/security-gate.js +0 -223
- package/dist/core/edits/verify-hook.js +0 -273
- package/dist/core/edits/worktree.js +0 -322
- package/dist/core/engine/adapter-runner.js +0 -8
- package/dist/core/engine/anvil-client.js +0 -344
- package/dist/core/engine/auto-compact.js +0 -179
- package/dist/core/engine/budgets.js +0 -195
- package/dist/core/engine/context-prefix.js +0 -155
- package/dist/core/engine/index.js +0 -12
- package/dist/core/engine/intensity.js +0 -163
- package/dist/core/engine/intent.js +0 -260
- package/dist/core/engine/native-pugi.js +0 -1616
- package/dist/core/engine/noop.js +0 -27
- package/dist/core/engine/prompts.js +0 -236
- package/dist/core/engine/strip-internal-fields.js +0 -124
- package/dist/core/engine/tool-bridge.js +0 -2173
- package/dist/core/engine/verification-patterns.js +0 -195
- package/dist/core/evaluation/golden-dataset.js +0 -293
- package/dist/core/feedback/queue.js +0 -177
- package/dist/core/feedback/submitter.js +0 -145
- package/dist/core/file-cache.js +0 -141
- package/dist/core/flatten/flatten-repo.js +0 -439
- package/dist/core/format/osc8-link.js +0 -28
- package/dist/core/hook-chains.js +0 -392
- package/dist/core/hooks/citation-verify-hook.js +0 -138
- package/dist/core/hooks/citation-verify.js +0 -112
- package/dist/core/hooks/events.js +0 -46
- package/dist/core/hooks/index.js +0 -15
- package/dist/core/hooks/registry.js +0 -216
- package/dist/core/hooks/runner.js +0 -236
- package/dist/core/hooks/v2/event-emitter.js +0 -115
- package/dist/core/hooks/v2/executor.js +0 -282
- package/dist/core/hooks/v2/index.js +0 -25
- package/dist/core/hooks/v2/lifecycle.js +0 -104
- package/dist/core/hooks/v2/loader.js +0 -216
- package/dist/core/hooks/v2/matcher.js +0 -125
- package/dist/core/hooks/v2/trust.js +0 -143
- package/dist/core/hooks/v2/types.js +0 -86
- package/dist/core/hooks/worktree-events.js +0 -158
- package/dist/core/hooks.js +0 -415
- package/dist/core/image/renderer.js +0 -71
- package/dist/core/index-store.js +0 -260
- package/dist/core/init/detector.js +0 -582
- package/dist/core/init/template-renderer.js +0 -242
- package/dist/core/jobs/registry.js +0 -462
- package/dist/core/ledger/results-tsv.js +0 -142
- package/dist/core/log-discipline/stdout-redirect.js +0 -51
- package/dist/core/lsp/cache.js +0 -105
- package/dist/core/lsp/client.js +0 -1229
- package/dist/core/lsp/language-detect.js +0 -66
- package/dist/core/lsp/post-edit-diagnostics.js +0 -171
- package/dist/core/lsp/server-detect.js +0 -173
- package/dist/core/lsp/symbol-cache.js +0 -162
- package/dist/core/lsp/symbol-tools.js +0 -664
- package/dist/core/mcp/client.js +0 -385
- package/dist/core/mcp/http-server.js +0 -553
- package/dist/core/mcp/orchestrator-config.js +0 -192
- package/dist/core/mcp/orchestrator-tools.js +0 -806
- package/dist/core/mcp/permission.js +0 -190
- package/dist/core/mcp/registry.js +0 -193
- package/dist/core/mcp/server-tools.js +0 -219
- package/dist/core/mcp/server.js +0 -397
- package/dist/core/mcp/trust.js +0 -91
- package/dist/core/memory/dual-write.js +0 -416
- package/dist/core/memory/passive-extract.js +0 -130
- package/dist/core/memory/phase1-kinds.js +0 -20
- package/dist/core/memory/secret-scanner.js +0 -304
- package/dist/core/memory-sync/queue.js +0 -170
- package/dist/core/metrics/extract.js +0 -113
- package/dist/core/modes/roo-modes.js +0 -68
- package/dist/core/onboarding/ensure-initialized.js +0 -133
- package/dist/core/onboarding/marker.js +0 -111
- package/dist/core/onboarding/telemetry-state.js +0 -108
- package/dist/core/output-style/presets.js +0 -176
- package/dist/core/output-style/state.js +0 -185
- package/dist/core/path-security.js +0 -345
- package/dist/core/permission.js +0 -369
- package/dist/core/permissions/auto-classifier.js +0 -124
- package/dist/core/permissions/bash-parser.js +0 -371
- package/dist/core/permissions/circuit-breaker.js +0 -83
- package/dist/core/permissions/constrained-edit.js +0 -91
- package/dist/core/permissions/gate.js +0 -278
- package/dist/core/permissions/index.js +0 -20
- package/dist/core/permissions/mode.js +0 -174
- package/dist/core/permissions/network-egress.js +0 -137
- package/dist/core/permissions/state.js +0 -241
- package/dist/core/permissions/tool-class.js +0 -107
- package/dist/core/plan-mode/ui-state.js +0 -51
- package/dist/core/plans/plan-artifact.js +0 -721
- package/dist/core/policy-limits/etag-store.js +0 -122
- package/dist/core/prd-check/parser.js +0 -215
- package/dist/core/prd-check/reporter.js +0 -127
- package/dist/core/prd-check/session-review.js +0 -557
- package/dist/core/prd-check/verifiers.js +0 -223
- package/dist/core/prompt-cache/client-cache.js +0 -99
- package/dist/core/prompts/assembly.js +0 -29
- package/dist/core/prompts/registry.js +0 -364
- package/dist/core/pugi-gitignore.js +0 -52
- package/dist/core/pugi-md/cc-compat-rules.js +0 -735
- package/dist/core/pugi-md/context-injector.js +0 -76
- package/dist/core/pugi-md/walk-up.js +0 -207
- package/dist/core/python/uv-installer.js +0 -270
- package/dist/core/python/uv-resolver.js +0 -83
- package/dist/core/rate-limit/narrator.js +0 -146
- package/dist/core/recipes/cli-types.js +0 -20
- package/dist/core/recipes/loader.js +0 -103
- package/dist/core/recipes/runner.js +0 -345
- package/dist/core/recipes/schema.js +0 -587
- package/dist/core/release-notes/parser.js +0 -241
- package/dist/core/release-notes/state.js +0 -116
- package/dist/core/repl/ask.js +0 -512
- package/dist/core/repl/cancellation.js +0 -98
- package/dist/core/repl/cap-warning.js +0 -91
- package/dist/core/repl/clipboard-read.js +0 -174
- package/dist/core/repl/dispatch-fsm.js +0 -220
- package/dist/core/repl/engine-bridge.js +0 -303
- package/dist/core/repl/history-search.js +0 -175
- package/dist/core/repl/history.js +0 -182
- package/dist/core/repl/kill-ring.js +0 -138
- package/dist/core/repl/model-pricing.js +0 -135
- package/dist/core/repl/privacy-banner.js +0 -71
- package/dist/core/repl/session.js +0 -4962
- package/dist/core/repl/slash-commands.js +0 -747
- package/dist/core/repl/store/index.js +0 -12
- package/dist/core/repl/store/jsonl-log.js +0 -321
- package/dist/core/repl/store/lockfile.js +0 -155
- package/dist/core/repl/store/session-store.js +0 -821
- package/dist/core/repl/store/types.js +0 -44
- package/dist/core/repl/store/uuid-v7.js +0 -68
- package/dist/core/repl/tool-route.js +0 -382
- package/dist/core/repl/workspace-context.js +0 -206
- package/dist/core/repo-map/build.js +0 -125
- package/dist/core/repo-map/cache.js +0 -185
- package/dist/core/repo-map/extractor.js +0 -254
- package/dist/core/repo-map/formatter.js +0 -145
- package/dist/core/repo-map/page-rank.js +0 -105
- package/dist/core/repo-map/scanner.js +0 -211
- package/dist/core/retro/git-collector.js +0 -251
- package/dist/core/retro/health-card.js +0 -25
- package/dist/core/retro/metrics.js +0 -342
- package/dist/core/retro/narrative.js +0 -249
- package/dist/core/retro/plane-collector.js +0 -274
- package/dist/core/retro/pr-issue-link.js +0 -65
- package/dist/core/retro/types.js +0 -16
- package/dist/core/retry-budget/budget.js +0 -284
- package/dist/core/retry-budget/index.js +0 -5
- package/dist/core/retry-budget/retry-cap.js +0 -74
- package/dist/core/routing/lead-worker.js +0 -43
- package/dist/core/routing/pre-flight-estimator.js +0 -108
- package/dist/core/runs/run-tree.js +0 -103
- package/dist/core/sandboxing/adapter.js +0 -29
- package/dist/core/sandboxing/index.js +0 -49
- package/dist/core/sandboxing/none.js +0 -19
- package/dist/core/sandboxing/seatbelt.js +0 -183
- package/dist/core/security/injection-scanner.js +0 -367
- package/dist/core/security/output-filter.js +0 -418
- package/dist/core/session/env-file.js +0 -105
- package/dist/core/session/section-budgets.js +0 -140
- package/dist/core/session.js +0 -377
- package/dist/core/settings.js +0 -400
- package/dist/core/share/formatter.js +0 -271
- package/dist/core/share/redactor.js +0 -221
- package/dist/core/share/uploader.js +0 -267
- package/dist/core/skills/defaults.js +0 -457
- package/dist/core/skills/loader.js +0 -454
- package/dist/core/skills/sources.js +0 -480
- package/dist/core/skills/trust.js +0 -172
- package/dist/core/smoke/headless-driver.js +0 -174
- package/dist/core/smoke/orchestrator.js +0 -194
- package/dist/core/smoke/runner.js +0 -238
- package/dist/core/smoke/scenario-parser.js +0 -316
- package/dist/core/statusline.js +0 -99
- package/dist/core/subagents/dispatcher-real.js +0 -600
- package/dist/core/subagents/dispatcher.js +0 -352
- package/dist/core/subagents/index.js +0 -39
- package/dist/core/subagents/isolation-matrix.js +0 -213
- package/dist/core/subagents/spawn.js +0 -101
- package/dist/core/telemetry/emitter.js +0 -229
- package/dist/core/telemetry/queue.js +0 -251
- package/dist/core/theme/context.js +0 -91
- package/dist/core/theme/presets.js +0 -228
- package/dist/core/theme/state.js +0 -181
- package/dist/core/todos/invariant.js +0 -10
- package/dist/core/todos/state.js +0 -177
- package/dist/core/tool-schema/compressor.js +0 -89
- package/dist/core/transport/version-interceptor.js +0 -166
- package/dist/core/trust.js +0 -109
- package/dist/core/tui/thinking-block.js +0 -64
- package/dist/core/vim/keymap.js +0 -288
- package/dist/core/vim/state.js +0 -92
- package/dist/core/watch-markers/marker-watcher.js +0 -133
- package/dist/core/worktree/include-parser.js +0 -249
- package/dist/core/worktree-manager/cleanup.js +0 -123
- package/dist/core/worktree-manager/manager.js +0 -303
- package/dist/index.js +0 -44
- package/dist/runtime/bootstrap.js +0 -190
- package/dist/runtime/cli.js +0 -8121
- package/dist/runtime/commands/agents.js +0 -385
- package/dist/runtime/commands/budget.js +0 -192
- package/dist/runtime/commands/cancel.js +0 -231
- package/dist/runtime/commands/chain.js +0 -489
- package/dist/runtime/commands/codegraph-status.js +0 -227
- package/dist/runtime/commands/compact.js +0 -297
- package/dist/runtime/commands/config.js +0 -595
- package/dist/runtime/commands/cost.js +0 -199
- package/dist/runtime/commands/delegate.js +0 -312
- package/dist/runtime/commands/dispatch.js +0 -126
- package/dist/runtime/commands/doctor.js +0 -579
- package/dist/runtime/commands/feedback.js +0 -184
- package/dist/runtime/commands/hooks.js +0 -187
- package/dist/runtime/commands/init.js +0 -254
- package/dist/runtime/commands/lsp.js +0 -368
- package/dist/runtime/commands/mcp.js +0 -935
- package/dist/runtime/commands/memory.js +0 -582
- package/dist/runtime/commands/model.js +0 -237
- package/dist/runtime/commands/onboarding.js +0 -275
- package/dist/runtime/commands/patch.js +0 -128
- package/dist/runtime/commands/permissions.js +0 -112
- package/dist/runtime/commands/plan.js +0 -143
- package/dist/runtime/commands/prd-check.js +0 -285
- package/dist/runtime/commands/privacy.js +0 -107
- package/dist/runtime/commands/recipe.js +0 -325
- package/dist/runtime/commands/redo-blob-store.js +0 -92
- package/dist/runtime/commands/redo.js +0 -361
- package/dist/runtime/commands/release-notes.js +0 -229
- package/dist/runtime/commands/repo-map.js +0 -95
- package/dist/runtime/commands/report.js +0 -299
- package/dist/runtime/commands/resume.js +0 -118
- package/dist/runtime/commands/review-consensus.js +0 -414
- package/dist/runtime/commands/rewind.js +0 -333
- package/dist/runtime/commands/roster.js +0 -117
- package/dist/runtime/commands/sessions.js +0 -163
- package/dist/runtime/commands/share.js +0 -316
- package/dist/runtime/commands/skills.js +0 -401
- package/dist/runtime/commands/status.js +0 -186
- package/dist/runtime/commands/stickers.js +0 -82
- package/dist/runtime/commands/style.js +0 -194
- package/dist/runtime/commands/theme.js +0 -196
- package/dist/runtime/commands/undo.js +0 -361
- package/dist/runtime/commands/update.js +0 -289
- package/dist/runtime/commands/vim.js +0 -140
- package/dist/runtime/commands/worktree.js +0 -177
- package/dist/runtime/commands/worktrees.js +0 -155
- package/dist/runtime/deprecation-warning.js +0 -69
- package/dist/runtime/engine-exit-code.js +0 -50
- package/dist/runtime/headless-repl.js +0 -195
- package/dist/runtime/headless.js +0 -548
- package/dist/runtime/load-hooks-or-exit.js +0 -71
- package/dist/runtime/plan-decompose.js +0 -531
- package/dist/runtime/sigint-guard.js +0 -272
- package/dist/runtime/stream-renderer.js +0 -195
- package/dist/runtime/update-check.js +0 -294
- package/dist/runtime/version.js +0 -65
- package/dist/runtime/worktree-bootstrap.js +0 -579
- package/dist/skills/bundled/batch.js +0 -617
- package/dist/skills/bundled/index.js +0 -45
- package/dist/skills/bundled/loop.js +0 -358
- package/dist/skills/bundled/remember.js +0 -383
- package/dist/skills/bundled/simplify.js +0 -289
- package/dist/skills/bundled/skillify.js +0 -373
- package/dist/skills/bundled/stuck.js +0 -558
- package/dist/skills/bundled/verify.js +0 -439
- package/dist/testing/vcr.js +0 -486
- package/dist/tools/agent-tool.js +0 -229
- package/dist/tools/apply-patch.js +0 -556
- package/dist/tools/ask-user-question.js +0 -337
- package/dist/tools/ask-user.js +0 -115
- package/dist/tools/bash.js +0 -1238
- package/dist/tools/brief.js +0 -224
- package/dist/tools/cron.js +0 -433
- package/dist/tools/enter-worktree.js +0 -250
- package/dist/tools/exit-worktree.js +0 -147
- package/dist/tools/file-tools.js +0 -553
- package/dist/tools/http-request.js +0 -336
- package/dist/tools/lsp-tools.js +0 -565
- package/dist/tools/mcp-tool.js +0 -260
- package/dist/tools/multi-edit.js +0 -361
- package/dist/tools/powershell.js +0 -268
- package/dist/tools/registry.js +0 -166
- package/dist/tools/server-tools.js +0 -892
- package/dist/tools/skill-tool.js +0 -96
- package/dist/tools/sleep.js +0 -99
- package/dist/tools/synthetic-output.js +0 -133
- package/dist/tools/tasks.js +0 -208
- package/dist/tools/todo-write.js +0 -184
- package/dist/tools/verify-plan-execution.js +0 -295
- package/dist/tools/web-fetch-injection-scanner.js +0 -207
- package/dist/tools/web-fetch.js +0 -720
- package/dist/tools/web-search.js +0 -458
- package/dist/tui/agent-progress-card.js +0 -111
- package/dist/tui/agent-tree-pane.js +0 -9
- package/dist/tui/agent-tree.js +0 -87
- package/dist/tui/ask-cli.js +0 -52
- package/dist/tui/ask-modal.js +0 -211
- package/dist/tui/ask-user-question-chips.js +0 -315
- package/dist/tui/ask-user-question-prompt.js +0 -203
- package/dist/tui/compact-banner.js +0 -81
- package/dist/tui/conversation-pane.js +0 -164
- package/dist/tui/cost-table.js +0 -111
- package/dist/tui/device-flow.js +0 -142
- package/dist/tui/doctor-table.js +0 -46
- package/dist/tui/feedback-prompt.js +0 -156
- package/dist/tui/input-box.js +0 -732
- package/dist/tui/login-picker.js +0 -69
- package/dist/tui/markdown-render.js +0 -266
- package/dist/tui/multi-file-diff-approval.js +0 -375
- package/dist/tui/onboarding-wizard.js +0 -240
- package/dist/tui/permissions-picker.js +0 -86
- package/dist/tui/render.js +0 -160
- package/dist/tui/repl-render.js +0 -770
- package/dist/tui/repl-splash-art.js +0 -64
- package/dist/tui/repl-splash-mascot.js +0 -154
- package/dist/tui/repl-splash.js +0 -117
- package/dist/tui/repl.js +0 -378
- package/dist/tui/slash-palette.js +0 -106
- package/dist/tui/splash-data.js +0 -61
- package/dist/tui/splash.js +0 -31
- package/dist/tui/status-bar.js +0 -209
- package/dist/tui/status-table.js +0 -7
- package/dist/tui/stickers-art.js +0 -136
- package/dist/tui/style-table.js +0 -28
- package/dist/tui/theme-table.js +0 -29
- package/dist/tui/thinking-spinner.js +0 -123
- package/dist/tui/tool-stream-pane.js +0 -140
- package/dist/tui/update-banner.js +0 -33
- package/dist/tui/vim-input.js +0 -267
- package/dist/tui/welcome-banner.js +0 -107
- package/dist/tui/welcome-data.js +0 -293
- package/dist/tui/workspace-context.js +0 -105
- package/docs/examples/codegraph.mcp.json +0 -10
- package/test/scenarios/codegen-create-file.scenario.txt +0 -13
- package/test/scenarios/compact-force.scenario.txt +0 -12
- package/test/scenarios/identity.scenario.txt +0 -11
- package/test/scenarios/persona-handoff.scenario.txt +0 -12
- package/test/scenarios/walkback.scenario.txt +0 -12
package/dist/tools/bash.js
DELETED
|
@@ -1,1238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Class-aware bash tool — Sprint .
|
|
3
|
-
*
|
|
4
|
-
* The agent loop invokes this tool through the registry name `bash`.
|
|
5
|
-
* It supersedes `file-tools.ts::bashTool`, which used the legacy
|
|
6
|
-
* blocklist gate. The tool-bridge wires this new entry point so the
|
|
7
|
-
* registry entry (`registry.ts` `bash`) is not duplicated.
|
|
8
|
-
*
|
|
9
|
-
* Behavioural changes vs the legacy tool:
|
|
10
|
-
* 1. Permission decision routes through `evaluateBashPermission`
|
|
11
|
-
* (7-class taxonomy, mode-aware, destructive override gate).
|
|
12
|
-
* 2. Output cap is 32 KB combined stdout+stderr per call (down
|
|
13
|
-
* from 64 KB). Overflow is persisted to
|
|
14
|
-
* `.pugi/artifacts/<sessionId>/bash-<callId>.out` with the path
|
|
15
|
-
* returned as `artifactRef`.
|
|
16
|
-
* 3. Cwd carry-over: the tool receives `cwd` from the previous
|
|
17
|
-
* turn's session state and writes the new cwd back when the
|
|
18
|
-
* command was a `cd <path>` that landed inside
|
|
19
|
-
* `workspaceRoot ∪ additionalDirectories`. Escapes reset the
|
|
20
|
-
* cwd to workspaceRoot and emit `bash.cwd_escape`.
|
|
21
|
-
* 4. Background jobs: when `background: true`, spawn detached,
|
|
22
|
-
* track in `~/.pugi/jobs.json`, return immediately with
|
|
23
|
-
* `jobId`. `listJobs()` and `killJob(jobId)` are exported.
|
|
24
|
-
* 5. 60s default timeout. SIGTERM at deadline, SIGKILL 5s later.
|
|
25
|
-
* Emit `bash.timeout`.
|
|
26
|
-
* 6. POSIX-only (`/bin/sh`). The non-goal in explicitly
|
|
27
|
-
* drops Windows shell support for M1.
|
|
28
|
-
*/
|
|
29
|
-
import { randomUUID } from 'node:crypto';
|
|
30
|
-
import { appendFileSync, closeSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from 'node:fs';
|
|
31
|
-
import { homedir } from 'node:os';
|
|
32
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
33
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
34
|
-
import { classifyBash } from '../core/bash-classifier.js';
|
|
35
|
-
import { applyRedirect, finaliseRedirectFile, normalizeTailLines, openRedirectFile, resolveRedirectTarget, } from '../core/bash/redirect.js';
|
|
36
|
-
import { evaluateBashPermission } from '../core/permission.js';
|
|
37
|
-
import { writeAuditEvent } from '../core/audit/audit-trail.js';
|
|
38
|
-
import { getJobRegistry, } from '../core/jobs/registry.js';
|
|
39
|
-
import { recordToolCall, recordToolResult } from '../core/session.js';
|
|
40
|
-
export const BASH_OUTPUT_CAP_BYTES = 32 * 1024;
|
|
41
|
-
export const BASH_DEFAULT_TIMEOUT_MS = 60_000;
|
|
42
|
-
export const BASH_SIGKILL_GRACE_MS = 5_000;
|
|
43
|
-
/**
|
|
44
|
-
* Mid-stream cap. The 32 KB BASH_OUTPUT_CAP_BYTES is the report cap;
|
|
45
|
-
* this is the in-memory ceiling beyond which we stop buffering and
|
|
46
|
-
* SIGTERM the child to prevent a `yes`-style stream from pinning
|
|
47
|
-
* 60+ MB before the timeout watchdog fires.
|
|
48
|
-
*
|
|
49
|
-
* Code Reviewer P1 retro: the async path previously
|
|
50
|
-
* accumulated stdout chunks without bound; only spawnSync had a
|
|
51
|
-
* 10 MB maxBuffer ceiling. Aligning the async path closes the gap.
|
|
52
|
-
*/
|
|
53
|
-
export const BASH_LIVE_OUTPUT_CAP_BYTES = 1024 * 1024;
|
|
54
|
-
/**
|
|
55
|
-
* Bash tool entry point. Returns the standard shape the engine loop
|
|
56
|
-
* consumes; throws only on argument-shape errors (e.g. negative
|
|
57
|
-
* timeouts) and otherwise surfaces the failure through
|
|
58
|
-
* `{ exitCode: 126, stderr }`.
|
|
59
|
-
*/
|
|
60
|
-
export async function bashTool(input, ctx) {
|
|
61
|
-
const cmd = input.cmd ?? '';
|
|
62
|
-
const additionalDirectories = ctx.additionalDirectories ?? [];
|
|
63
|
-
const source = ctx.source ?? 'agent';
|
|
64
|
-
const toolCallId = recordToolCall(ctx.session, 'bash', cmd);
|
|
65
|
-
// Cwd carry-over decision (also re-checked post-run).
|
|
66
|
-
const startCwd = resolveStartCwd(input.cwd ?? ctx.lastBashCwd, ctx.root, additionalDirectories);
|
|
67
|
-
// Workspace-git-boundary guard (CEO P0 #51).
|
|
68
|
-
// Runs BEFORE the permission gate so the boundary escape message is
|
|
69
|
-
// the one the operator/engine sees, regardless of permission policy.
|
|
70
|
-
// The leak is structural (git silently writes to an ancestor .git
|
|
71
|
-
// when the workspace lacks one), not a policy violation, so the
|
|
72
|
-
// diagnostic must surface even when the permission gate would
|
|
73
|
-
// otherwise have asked or auto-allowed.
|
|
74
|
-
const boundaryBlock = enforceGitBoundary(cmd, startCwd, ctx.root);
|
|
75
|
-
if (boundaryBlock !== null) {
|
|
76
|
-
emitEvent(ctx.session, 'bash.git_boundary_escape', {
|
|
77
|
-
cmd,
|
|
78
|
-
workspaceRoot: ctx.root,
|
|
79
|
-
resolvedToplevel: boundaryBlock.resolvedToplevel ?? null,
|
|
80
|
-
});
|
|
81
|
-
recordToolResult(ctx.session, toolCallId, 'error', boundaryBlock.reason);
|
|
82
|
-
return {
|
|
83
|
-
stdout: '',
|
|
84
|
-
stderr: boundaryBlock.reason,
|
|
85
|
-
exitCode: 126,
|
|
86
|
-
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
87
|
-
truncated: false,
|
|
88
|
-
timedOut: false,
|
|
89
|
-
cancelled: false,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
// Permission gate via the new class-aware engine.
|
|
93
|
-
const decision = evaluateBashPermission(cmd, ctx.settings.permissions.mode, {
|
|
94
|
-
workspaceRoot: ctx.root,
|
|
95
|
-
additionalDirectories,
|
|
96
|
-
source,
|
|
97
|
-
});
|
|
98
|
-
if (decision.decision !== 'allow') {
|
|
99
|
-
const reason = `Permission ${decision.decision}: ${decision.reason}`;
|
|
100
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
101
|
-
// #21 : emit `permission_denied` to
|
|
102
|
-
// the tenant-wide audit trail. Truncate the cmd preview to 200
|
|
103
|
-
// chars so a long here-doc does not bloat the JSONL row; the
|
|
104
|
-
// session log keeps the full text for forensic replay.
|
|
105
|
-
writeAuditEvent({
|
|
106
|
-
event: 'permission_denied',
|
|
107
|
-
sessionId: ctx.session.id,
|
|
108
|
-
workspaceRoot: ctx.root,
|
|
109
|
-
data: {
|
|
110
|
-
tool: 'bash',
|
|
111
|
-
source,
|
|
112
|
-
decision: decision.decision,
|
|
113
|
-
reason: decision.reason,
|
|
114
|
-
cmdPreview: cmd.slice(0, 200),
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
return {
|
|
118
|
-
stdout: '',
|
|
119
|
-
stderr: `Permission denied: ${decision.reason}`,
|
|
120
|
-
exitCode: 126,
|
|
121
|
-
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
122
|
-
truncated: false,
|
|
123
|
-
timedOut: false,
|
|
124
|
-
cancelled: false,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
// CEO P1 #25 — pre-spawn cancellation check. Fires
|
|
128
|
-
// AFTER the permission gate so a cancelled brief never reaches
|
|
129
|
-
// /bin/sh even when the command would have been allowed. Mirrors
|
|
130
|
-
// the `gateOnCancellation` pattern from file-tools.ts.
|
|
131
|
-
if (ctx.cancellation?.isAborted === true) {
|
|
132
|
-
const reason = 'operator_aborted: bash refused before spawn';
|
|
133
|
-
emitEvent(ctx.session, 'bash.cancelled', { cmd, phase: 'pre_spawn' });
|
|
134
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
|
|
135
|
-
return {
|
|
136
|
-
stdout: '',
|
|
137
|
-
stderr: reason,
|
|
138
|
-
exitCode: 130,
|
|
139
|
-
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
140
|
-
truncated: false,
|
|
141
|
-
timedOut: false,
|
|
142
|
-
cancelled: true,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
// Background job branch.
|
|
146
|
-
if (input.background === true) {
|
|
147
|
-
return runBackground({ cmd, ctx, toolCallId, startCwd, additionalDirectories });
|
|
148
|
-
}
|
|
149
|
-
// Foreground branch with timeout watchdog.
|
|
150
|
-
const timeoutMs = sanitizeTimeout(input.timeoutMs);
|
|
151
|
-
const childEnv = buildChildEnv();
|
|
152
|
-
// Pugi backlog P2 — redirect path. When the caller opted into
|
|
153
|
-
// stdout redirect, we open a write-only fd at the resolved log
|
|
154
|
-
// path and hand it directly to the child's stdio array so the
|
|
155
|
-
// child writes through the kernel pipe → file fd without buffering
|
|
156
|
-
// hundreds of MB in the Node process. The buffered code path below
|
|
157
|
-
// is the fallback for callers that did not opt in.
|
|
158
|
-
let redirectState = null;
|
|
159
|
-
if (input.redirect !== undefined) {
|
|
160
|
-
try {
|
|
161
|
-
const target = resolveRedirectTarget({
|
|
162
|
-
workspaceRoot: ctx.root,
|
|
163
|
-
sessionId: ctx.session.id,
|
|
164
|
-
toolCallId,
|
|
165
|
-
command: cmd,
|
|
166
|
-
override: input.redirect.path,
|
|
167
|
-
});
|
|
168
|
-
const { fd, tempPath } = openRedirectFile(target);
|
|
169
|
-
redirectState = {
|
|
170
|
-
target,
|
|
171
|
-
fd,
|
|
172
|
-
tempPath,
|
|
173
|
-
tailLines: normalizeTailLines(input.redirect.tailLines),
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
// Bad caller-supplied path (absolute, traversal escape). Fall
|
|
178
|
-
// back to a structured error rather than crashing the engine
|
|
179
|
-
// loop. Mirrors how the permission gate surfaces a refusal —
|
|
180
|
-
// the model can adjust the redirect spec and retry.
|
|
181
|
-
const reason = `redirect refused: ${error.message}`;
|
|
182
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
183
|
-
return {
|
|
184
|
-
stdout: '',
|
|
185
|
-
stderr: reason,
|
|
186
|
-
exitCode: 126,
|
|
187
|
-
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
188
|
-
truncated: false,
|
|
189
|
-
timedOut: false,
|
|
190
|
-
cancelled: false,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// POSIX-only `/bin/sh -c <cmd>`. The non-goals explicitly
|
|
195
|
-
// exclude Windows for M1.
|
|
196
|
-
//
|
|
197
|
-
// stdio layout:
|
|
198
|
-
// - default: ['ignore', 'pipe', 'pipe'] — buffer chunks in
|
|
199
|
-
// Node so the post-run capToCombined can size them
|
|
200
|
-
// to the report cap.
|
|
201
|
-
// - redirect: ['ignore', fd, fd] — kernel pipes stdout+stderr
|
|
202
|
-
// straight into the log file fd. No Node-side
|
|
203
|
-
// buffering, no truncation marker, no in-memory
|
|
204
|
-
// ceiling. The tail-reader fishes the trailing
|
|
205
|
-
// lines out of the file after the child exits.
|
|
206
|
-
const stdioLayout = redirectState !== null
|
|
207
|
-
? ['ignore', redirectState.fd, redirectState.fd]
|
|
208
|
-
: ['ignore', 'pipe', 'pipe'];
|
|
209
|
-
const child = spawn('/bin/sh', ['-c', cmd], {
|
|
210
|
-
cwd: startCwd,
|
|
211
|
-
env: childEnv,
|
|
212
|
-
stdio: stdioLayout,
|
|
213
|
-
detached: false,
|
|
214
|
-
});
|
|
215
|
-
const stdoutChunks = [];
|
|
216
|
-
const stderrChunks = [];
|
|
217
|
-
let stdoutBytes = 0;
|
|
218
|
-
let stderrBytes = 0;
|
|
219
|
-
// We keep collecting beyond the report cap (BASH_OUTPUT_CAP_BYTES)
|
|
220
|
-
// for the artifact-overflow file but flag `truncated` so the
|
|
221
|
-
// agent-facing payload is the head. To prevent a runaway producer
|
|
222
|
-
// (`yes`, `cat /dev/urandom`) from pinning hundreds of megabytes
|
|
223
|
-
// before the timeout watchdog fires, we enforce a live ceiling
|
|
224
|
-
// (BASH_LIVE_OUTPUT_CAP_BYTES) and SIGTERM the child when crossed.
|
|
225
|
-
let truncatedMidStream = false;
|
|
226
|
-
// CEO P1 #25 — mid-stream operator cancellation. The
|
|
227
|
-
// listener registered against the CancellationToken below flips
|
|
228
|
-
// this flag and SIGTERMs the child. The close handler reads it to
|
|
229
|
-
// decide between `cancelled` (operator abort) and `timedOut`
|
|
230
|
-
// (watchdog).
|
|
231
|
-
let cancelledMidStream = false;
|
|
232
|
-
const enforceLiveCap = () => {
|
|
233
|
-
if (truncatedMidStream)
|
|
234
|
-
return;
|
|
235
|
-
if (stdoutBytes + stderrBytes <= BASH_LIVE_OUTPUT_CAP_BYTES)
|
|
236
|
-
return;
|
|
237
|
-
truncatedMidStream = true;
|
|
238
|
-
try {
|
|
239
|
-
child.kill('SIGTERM');
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
// child already exited; the close handler will run
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
// CEO P1 #25 — live stream callback. When the REPL
|
|
246
|
-
// host wires `onStreamChunk`, we forward each stdout/stderr chunk
|
|
247
|
-
// in real time so the conversation pane / tool-stream pane paint
|
|
248
|
-
// bytes as they arrive instead of waiting for the child to exit.
|
|
249
|
-
// We invoke the callback inside a try/catch so a buggy sink
|
|
250
|
-
// (renderer crash, assertion error) never escalates to killing
|
|
251
|
-
// the bash dispatch. The buffered path below still captures the
|
|
252
|
-
// chunk so the model + audit trail stay consistent regardless of
|
|
253
|
-
// renderer health.
|
|
254
|
-
const onStreamChunk = ctx.onStreamChunk;
|
|
255
|
-
const emitStreamChunk = onStreamChunk
|
|
256
|
-
? (stream, chunk) => {
|
|
257
|
-
try {
|
|
258
|
-
onStreamChunk({ stream, data: chunk.toString('utf8') });
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
// Sink crash — swallow.
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
: null;
|
|
265
|
-
// When redirect is on, child.stdout / child.stderr are null
|
|
266
|
-
// because the spawn handed the log-file fd in directly. The data
|
|
267
|
-
// listeners only fire on the buffered path, which is exactly what
|
|
268
|
-
// we want — the redirect contract is "no in-memory buffer, full
|
|
269
|
-
// output goes to disk".
|
|
270
|
-
if (redirectState === null) {
|
|
271
|
-
child.stdout?.on('data', (chunk) => {
|
|
272
|
-
if (truncatedMidStream || cancelledMidStream)
|
|
273
|
-
return;
|
|
274
|
-
stdoutChunks.push(chunk);
|
|
275
|
-
stdoutBytes += chunk.length;
|
|
276
|
-
if (emitStreamChunk)
|
|
277
|
-
emitStreamChunk('stdout', chunk);
|
|
278
|
-
enforceLiveCap();
|
|
279
|
-
});
|
|
280
|
-
child.stderr?.on('data', (chunk) => {
|
|
281
|
-
if (truncatedMidStream || cancelledMidStream)
|
|
282
|
-
return;
|
|
283
|
-
stderrChunks.push(chunk);
|
|
284
|
-
stderrBytes += chunk.length;
|
|
285
|
-
if (emitStreamChunk)
|
|
286
|
-
emitStreamChunk('stderr', chunk);
|
|
287
|
-
enforceLiveCap();
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
// CEO P1 #25 — wire the cancellation token to SIGTERM. We track
|
|
291
|
-
// the detach handle so a successful run releases the listener
|
|
292
|
-
// instead of leaving it pinned to a long-lived REPL
|
|
293
|
-
// CancellationToken (same anti-leak pattern as
|
|
294
|
-
// native-pugi.ts:262).
|
|
295
|
-
let detachCancelListener;
|
|
296
|
-
if (ctx.cancellation && !ctx.cancellation.isAborted) {
|
|
297
|
-
const onAbort = () => {
|
|
298
|
-
if (cancelledMidStream)
|
|
299
|
-
return;
|
|
300
|
-
cancelledMidStream = true;
|
|
301
|
-
emitEvent(ctx.session, 'bash.cancelled', { cmd, phase: 'mid_stream' });
|
|
302
|
-
try {
|
|
303
|
-
child.kill('SIGTERM');
|
|
304
|
-
}
|
|
305
|
-
catch {
|
|
306
|
-
// child already exited; close handler will run
|
|
307
|
-
}
|
|
308
|
-
// SIGKILL escalation if the child does not honour SIGTERM
|
|
309
|
-
// within the grace window. Mirrors the timeout watchdog's
|
|
310
|
-
// two-phase shutdown.
|
|
311
|
-
setTimeout(() => {
|
|
312
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
313
|
-
return;
|
|
314
|
-
try {
|
|
315
|
-
child.kill('SIGKILL');
|
|
316
|
-
}
|
|
317
|
-
catch {
|
|
318
|
-
// gone between the check and the signal
|
|
319
|
-
}
|
|
320
|
-
}, BASH_SIGKILL_GRACE_MS).unref();
|
|
321
|
-
};
|
|
322
|
-
detachCancelListener = ctx.cancellation.onAbort(onAbort);
|
|
323
|
-
}
|
|
324
|
-
const timeoutOutcome = await waitWithTimeout(child, timeoutMs);
|
|
325
|
-
// Detach the cancellation listener on completion so a long-lived
|
|
326
|
-
// REPL token does not retain a reference to the dead child + this
|
|
327
|
-
// closure.
|
|
328
|
-
if (detachCancelListener) {
|
|
329
|
-
try {
|
|
330
|
-
detachCancelListener();
|
|
331
|
-
}
|
|
332
|
-
catch { /* listener already drained */ }
|
|
333
|
-
}
|
|
334
|
-
// Pugi backlog P2 — redirect path. Close the log fd, rename
|
|
335
|
-
// the temp file into place, and return the envelope before the
|
|
336
|
-
// buffered-path code paths run. We do this for every exit shape
|
|
337
|
-
// (success, non-zero, timeout, cancel) so the log file always
|
|
338
|
-
// lands on disk and the tail reflects whatever the child produced
|
|
339
|
-
// before termination. The cancel/timeout branches still surface
|
|
340
|
-
// the appropriate exitCode through the envelope; the operator
|
|
341
|
-
// discovers the failure via `tail` + `exitCode`, not via the
|
|
342
|
-
// legacy stdout/stderr strings.
|
|
343
|
-
if (redirectState !== null) {
|
|
344
|
-
// Close our copy of the fd before rename so the inode is no
|
|
345
|
-
// longer held open by the parent process. The child's stdio
|
|
346
|
-
// already inherited a separate fd; closing ours does not affect
|
|
347
|
-
// the child's writes that already happened.
|
|
348
|
-
try {
|
|
349
|
-
closeSync(redirectState.fd);
|
|
350
|
-
}
|
|
351
|
-
catch {
|
|
352
|
-
// already closed (shouldn't happen on the happy path)
|
|
353
|
-
}
|
|
354
|
-
try {
|
|
355
|
-
finaliseRedirectFile(redirectState.target, redirectState.tempPath);
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
// best-effort — the temp file still exists on disk for the
|
|
359
|
-
// operator to inspect even if the rename failed.
|
|
360
|
-
}
|
|
361
|
-
const redirectExitCode = cancelledMidStream
|
|
362
|
-
? 130
|
|
363
|
-
: timeoutOutcome.timedOut
|
|
364
|
-
? 124
|
|
365
|
-
: timeoutOutcome.exitCode;
|
|
366
|
-
const envelope = applyRedirect({
|
|
367
|
-
target: redirectState.target,
|
|
368
|
-
exitCode: redirectExitCode,
|
|
369
|
-
tailLines: redirectState.tailLines,
|
|
370
|
-
});
|
|
371
|
-
const nextCwdRedirect = computeNextCwd(cmd, startCwd, ctx.root, additionalDirectories, ctx.session);
|
|
372
|
-
// Emit the same lifecycle events the buffered path emits so the
|
|
373
|
-
// session audit trail is symmetric across redirect vs non-redirect
|
|
374
|
-
// dispatches.
|
|
375
|
-
if (cancelledMidStream) {
|
|
376
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', `operator_aborted: bash killed mid-stream (redirect=${envelope.logPath})`);
|
|
377
|
-
}
|
|
378
|
-
else if (timeoutOutcome.timedOut) {
|
|
379
|
-
emitEvent(ctx.session, 'bash.timeout', { cmd, timeoutMs });
|
|
380
|
-
recordToolResult(ctx.session, toolCallId, 'error', `bash timed out after ${timeoutMs}ms (redirect=${envelope.logPath})`);
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
recordToolResult(ctx.session, toolCallId, 'success', `bash exit=${redirectExitCode} redirect=${envelope.logPath}`);
|
|
384
|
-
}
|
|
385
|
-
return {
|
|
386
|
-
stdout: envelope.stdout,
|
|
387
|
-
stderr: envelope.stderr,
|
|
388
|
-
exitCode: redirectExitCode,
|
|
389
|
-
nextCwd: nextCwdRedirect,
|
|
390
|
-
truncated: envelope.truncated,
|
|
391
|
-
timedOut: timeoutOutcome.timedOut,
|
|
392
|
-
cancelled: cancelledMidStream,
|
|
393
|
-
logPath: envelope.logPath,
|
|
394
|
-
tail: envelope.tail,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
const stdoutFull = Buffer.concat(stdoutChunks).toString('utf8');
|
|
398
|
-
const stderrFull = Buffer.concat(stderrChunks).toString('utf8');
|
|
399
|
-
const combinedBytes = stdoutBytes + stderrBytes;
|
|
400
|
-
const truncated = combinedBytes > BASH_OUTPUT_CAP_BYTES || truncatedMidStream;
|
|
401
|
-
// Cwd carry-over: detect `cd <path> && <rest>` shapes from the
|
|
402
|
-
// command itself (we cannot observe the child's final cwd without
|
|
403
|
-
// a wrapper script). The classifier already flagged escapes; we
|
|
404
|
-
// re-validate here against allowed roots.
|
|
405
|
-
const nextCwd = computeNextCwd(cmd, startCwd, ctx.root, additionalDirectories, ctx.session);
|
|
406
|
-
// Overflow artifact when needed.
|
|
407
|
-
let artifactRef;
|
|
408
|
-
let stdoutOut = stdoutFull;
|
|
409
|
-
let stderrOut = stderrFull;
|
|
410
|
-
if (truncated) {
|
|
411
|
-
artifactRef = persistOverflow({
|
|
412
|
-
root: ctx.root,
|
|
413
|
-
sessionId: ctx.session.id,
|
|
414
|
-
toolCallId,
|
|
415
|
-
stdout: stdoutFull,
|
|
416
|
-
stderr: stderrFull,
|
|
417
|
-
});
|
|
418
|
-
stdoutOut = capToCombined(stdoutFull, stderrFull).stdout;
|
|
419
|
-
stderrOut = capToCombined(stdoutFull, stderrFull).stderr;
|
|
420
|
-
}
|
|
421
|
-
// CEO P1 #25 — cancellation wins races against timeout / cap
|
|
422
|
-
// overflow. The token already aborted by the time the close
|
|
423
|
-
// handler fires; we distinguish operator-driven termination from
|
|
424
|
-
// the watchdog so the REPL transcript reads "Aborted." rather
|
|
425
|
-
// than "Timed out."
|
|
426
|
-
if (cancelledMidStream) {
|
|
427
|
-
const reason = 'operator_aborted: bash killed mid-stream';
|
|
428
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
|
|
429
|
-
return {
|
|
430
|
-
stdout: stdoutOut,
|
|
431
|
-
stderr: stderrOut === '' ? reason : `${stderrOut}\n${reason}`,
|
|
432
|
-
exitCode: 130,
|
|
433
|
-
artifactRef,
|
|
434
|
-
nextCwd,
|
|
435
|
-
truncated,
|
|
436
|
-
timedOut: false,
|
|
437
|
-
cancelled: true,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
if (truncatedMidStream) {
|
|
441
|
-
// We killed the child because output cap exceeded mid-stream.
|
|
442
|
-
// Report that as the failure cause rather than as a timeout —
|
|
443
|
-
// the watchdog never fired, only our cap enforcer did.
|
|
444
|
-
const reason = `bash output cap exceeded mid-stream (cap=${BASH_LIVE_OUTPUT_CAP_BYTES} bytes)`;
|
|
445
|
-
emitEvent(ctx.session, 'bash.output_cap_exceeded', {
|
|
446
|
-
cmd,
|
|
447
|
-
capBytes: BASH_LIVE_OUTPUT_CAP_BYTES,
|
|
448
|
-
});
|
|
449
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
450
|
-
return {
|
|
451
|
-
stdout: stdoutOut,
|
|
452
|
-
stderr: stderrOut === '' ? reason : `${stderrOut}\n${reason}`,
|
|
453
|
-
exitCode: 137,
|
|
454
|
-
artifactRef,
|
|
455
|
-
nextCwd,
|
|
456
|
-
truncated: true,
|
|
457
|
-
timedOut: false,
|
|
458
|
-
cancelled: false,
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
if (timeoutOutcome.timedOut) {
|
|
462
|
-
emitEvent(ctx.session, 'bash.timeout', { cmd, timeoutMs });
|
|
463
|
-
recordToolResult(ctx.session, toolCallId, 'error', `bash timed out after ${timeoutMs}ms`);
|
|
464
|
-
return {
|
|
465
|
-
stdout: stdoutOut,
|
|
466
|
-
stderr: stderrOut === '' ? `bash timed out after ${timeoutMs}ms` : `${stderrOut}\nbash timed out after ${timeoutMs}ms`,
|
|
467
|
-
exitCode: 124,
|
|
468
|
-
artifactRef,
|
|
469
|
-
nextCwd,
|
|
470
|
-
truncated,
|
|
471
|
-
timedOut: true,
|
|
472
|
-
cancelled: false,
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
const exitCode = timeoutOutcome.exitCode;
|
|
476
|
-
recordToolResult(ctx.session, toolCallId, 'success', `bash exit=${exitCode} bytes=${combinedBytes}${artifactRef ? ` overflow=${artifactRef}` : ''}`);
|
|
477
|
-
return {
|
|
478
|
-
stdout: stdoutOut,
|
|
479
|
-
stderr: stderrOut,
|
|
480
|
-
exitCode,
|
|
481
|
-
artifactRef,
|
|
482
|
-
nextCwd,
|
|
483
|
-
truncated,
|
|
484
|
-
timedOut: false,
|
|
485
|
-
cancelled: false,
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
function sanitizeTimeout(value) {
|
|
489
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
490
|
-
return BASH_DEFAULT_TIMEOUT_MS;
|
|
491
|
-
}
|
|
492
|
-
// Cap user-supplied timeouts at 15 minutes so a runaway tool call
|
|
493
|
-
// cannot wedge the engine loop.
|
|
494
|
-
return Math.min(value, 15 * 60 * 1000);
|
|
495
|
-
}
|
|
496
|
-
function buildChildEnv() {
|
|
497
|
-
const childEnv = {};
|
|
498
|
-
const SAFE_ENV_ALLOW = new Set([
|
|
499
|
-
'PATH',
|
|
500
|
-
'HOME',
|
|
501
|
-
'USER',
|
|
502
|
-
'LOGNAME',
|
|
503
|
-
'SHELL',
|
|
504
|
-
'LANG',
|
|
505
|
-
'TZ',
|
|
506
|
-
'TERM',
|
|
507
|
-
'PWD',
|
|
508
|
-
]);
|
|
509
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
510
|
-
if (value === undefined)
|
|
511
|
-
continue;
|
|
512
|
-
if (SAFE_ENV_ALLOW.has(key) || key.startsWith('LC_')) {
|
|
513
|
-
childEnv[key] = value;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return childEnv;
|
|
517
|
-
}
|
|
518
|
-
function resolveStartCwd(requested, root, additionalDirectories) {
|
|
519
|
-
if (!requested)
|
|
520
|
-
return root;
|
|
521
|
-
const absolute = isAbsolute(requested) ? requested : resolve(root, requested);
|
|
522
|
-
const allowedRoots = [root, ...additionalDirectories];
|
|
523
|
-
for (const allowedRaw of allowedRoots) {
|
|
524
|
-
const allowed = allowedRaw.endsWith('/') ? allowedRaw.slice(0, -1) : allowedRaw;
|
|
525
|
-
if (absolute === allowed || absolute.startsWith(`${allowed}/`)) {
|
|
526
|
-
return absolute;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
return root;
|
|
530
|
-
}
|
|
531
|
-
function computeNextCwd(cmd, startCwd, root, additionalDirectories, session) {
|
|
532
|
-
// We mirror the classifier's view: only `cd <path>` at the head of
|
|
533
|
-
// the command updates cwd for the next turn. We do not attempt to
|
|
534
|
-
// chase `cd` calls that fired inside subshells or compound chains
|
|
535
|
-
// (`(cd foo && ls)` leaves the parent cwd untouched).
|
|
536
|
-
const firstComponent = cmd.trim().split(/\s*(?:&&|\|\||;|\|)\s*/)[0]?.trim() ?? '';
|
|
537
|
-
const match = firstComponent.match(/^cd(?:\s+(\S+))?\s*$/);
|
|
538
|
-
if (!match)
|
|
539
|
-
return startCwd;
|
|
540
|
-
const target = match[1];
|
|
541
|
-
if (target === undefined || target === '-' || target === '~') {
|
|
542
|
-
emitEvent(session, 'bash.cwd_escape', { cmd, reason: 'cd to HOME or last dir' });
|
|
543
|
-
return root;
|
|
544
|
-
}
|
|
545
|
-
const resolved = isAbsolute(target) || target.startsWith('~')
|
|
546
|
-
? resolve(target.startsWith('~') ? target.replace(/^~/, homedir()) : target)
|
|
547
|
-
: resolve(startCwd, target);
|
|
548
|
-
const allowedRoots = [root, ...additionalDirectories];
|
|
549
|
-
for (const allowedRaw of allowedRoots) {
|
|
550
|
-
const allowed = allowedRaw.endsWith('/') ? allowedRaw.slice(0, -1) : allowedRaw;
|
|
551
|
-
if (resolved === allowed || resolved.startsWith(`${allowed}/`)) {
|
|
552
|
-
return resolved;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
emitEvent(session, 'bash.cwd_escape', { cmd, reason: 'cd target outside workspace' });
|
|
556
|
-
return root;
|
|
557
|
-
}
|
|
558
|
-
function emitEvent(session, name, body) {
|
|
559
|
-
if (!session.enabled)
|
|
560
|
-
return;
|
|
561
|
-
const line = JSON.stringify({
|
|
562
|
-
id: randomUUID(),
|
|
563
|
-
sessionId: session.id,
|
|
564
|
-
timestamp: new Date().toISOString(),
|
|
565
|
-
type: 'bash',
|
|
566
|
-
name,
|
|
567
|
-
...body,
|
|
568
|
-
});
|
|
569
|
-
try {
|
|
570
|
-
appendFileSync(session.eventsPath, `${line}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
571
|
-
}
|
|
572
|
-
catch {
|
|
573
|
-
// Event log is best-effort; never crash the tool because of it.
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
function persistOverflow(input) {
|
|
577
|
-
const dir = join(input.root, '.pugi', 'artifacts', input.sessionId);
|
|
578
|
-
try {
|
|
579
|
-
mkdirSync(dir, { recursive: true });
|
|
580
|
-
const path = join(dir, `bash-${input.toolCallId}.out`);
|
|
581
|
-
const body = `--- stdout ---\n${input.stdout}\n--- stderr ---\n${input.stderr}\n`;
|
|
582
|
-
writeFileSync(path, body, { encoding: 'utf8', mode: 0o600 });
|
|
583
|
-
return path;
|
|
584
|
-
}
|
|
585
|
-
catch {
|
|
586
|
-
return '';
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
function capToCombined(stdout, stderr) {
|
|
590
|
-
// Split the budget proportionally so the head of each stream is
|
|
591
|
-
// preserved. When one stream is empty the other gets the full
|
|
592
|
-
// budget.
|
|
593
|
-
if (stdout.length + stderr.length <= BASH_OUTPUT_CAP_BYTES) {
|
|
594
|
-
return { stdout, stderr };
|
|
595
|
-
}
|
|
596
|
-
if (stdout.length === 0) {
|
|
597
|
-
return { stdout: '', stderr: trimWithMarker(stderr, BASH_OUTPUT_CAP_BYTES) };
|
|
598
|
-
}
|
|
599
|
-
if (stderr.length === 0) {
|
|
600
|
-
return { stdout: trimWithMarker(stdout, BASH_OUTPUT_CAP_BYTES), stderr: '' };
|
|
601
|
-
}
|
|
602
|
-
const total = stdout.length + stderr.length;
|
|
603
|
-
const stdoutBudget = Math.max(1024, Math.floor((stdout.length / total) * BASH_OUTPUT_CAP_BYTES));
|
|
604
|
-
const stderrBudget = BASH_OUTPUT_CAP_BYTES - stdoutBudget;
|
|
605
|
-
return {
|
|
606
|
-
stdout: trimWithMarker(stdout, stdoutBudget),
|
|
607
|
-
stderr: trimWithMarker(stderr, stderrBudget),
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
function trimWithMarker(text, budget) {
|
|
611
|
-
if (text.length <= budget)
|
|
612
|
-
return text;
|
|
613
|
-
return `${text.slice(0, budget)}\n(...truncated at ${budget} bytes; full output in artifactRef)`;
|
|
614
|
-
}
|
|
615
|
-
async function waitWithTimeout(child, timeoutMs) {
|
|
616
|
-
return await new Promise((resolvePromise) => {
|
|
617
|
-
let settled = false;
|
|
618
|
-
const sigtermTimer = setTimeout(() => {
|
|
619
|
-
if (settled)
|
|
620
|
-
return;
|
|
621
|
-
try {
|
|
622
|
-
child.kill('SIGTERM');
|
|
623
|
-
}
|
|
624
|
-
catch {
|
|
625
|
-
// child already exited
|
|
626
|
-
}
|
|
627
|
-
const sigkillTimer = setTimeout(() => {
|
|
628
|
-
if (settled)
|
|
629
|
-
return;
|
|
630
|
-
try {
|
|
631
|
-
child.kill('SIGKILL');
|
|
632
|
-
}
|
|
633
|
-
catch {
|
|
634
|
-
// already gone
|
|
635
|
-
}
|
|
636
|
-
}, BASH_SIGKILL_GRACE_MS);
|
|
637
|
-
sigkillTimer.unref();
|
|
638
|
-
}, timeoutMs);
|
|
639
|
-
sigtermTimer.unref();
|
|
640
|
-
const onClose = (code, signal) => {
|
|
641
|
-
if (settled)
|
|
642
|
-
return;
|
|
643
|
-
settled = true;
|
|
644
|
-
clearTimeout(sigtermTimer);
|
|
645
|
-
if (signal === 'SIGTERM' || signal === 'SIGKILL') {
|
|
646
|
-
// Heuristic: if we sent the signal because of the timer,
|
|
647
|
-
// report timeout. If the child raced and exited with the
|
|
648
|
-
// signal anyway (e.g. user pressed ^C through SIGINT trap)
|
|
649
|
-
// we still report timeout when the wall-clock crossed.
|
|
650
|
-
resolvePromise({ timedOut: true, exitCode: 124 });
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
resolvePromise({ timedOut: false, exitCode: code ?? 1 });
|
|
654
|
-
};
|
|
655
|
-
child.on('close', onClose);
|
|
656
|
-
child.on('error', () => {
|
|
657
|
-
if (settled)
|
|
658
|
-
return;
|
|
659
|
-
settled = true;
|
|
660
|
-
clearTimeout(sigtermTimer);
|
|
661
|
-
resolvePromise({ timedOut: false, exitCode: 1 });
|
|
662
|
-
});
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
function runBackground(input) {
|
|
666
|
-
const { cmd, ctx, toolCallId, startCwd } = input;
|
|
667
|
-
const childEnv = buildChildEnv();
|
|
668
|
-
const child = spawn('/bin/sh', ['-c', cmd], {
|
|
669
|
-
cwd: startCwd,
|
|
670
|
-
env: childEnv,
|
|
671
|
-
stdio: 'ignore',
|
|
672
|
-
detached: true,
|
|
673
|
-
});
|
|
674
|
-
child.unref();
|
|
675
|
-
const jobId = `pj-${randomUUID()}`;
|
|
676
|
-
const classification = classifyBash(cmd, {
|
|
677
|
-
workspaceRoot: ctx.root,
|
|
678
|
-
additionalDirectories: input.additionalDirectories,
|
|
679
|
-
});
|
|
680
|
-
const registry = getJobRegistry();
|
|
681
|
-
// Persist into the new registry. The promise is intentionally not
|
|
682
|
-
// awaited — `runBackground` is a synchronous control path inside the
|
|
683
|
-
// bash tool's async wrapper. The registry's atomic write is
|
|
684
|
-
// synchronous under the hood so the ledger is consistent before the
|
|
685
|
-
// caller observes the returned jobId, even when we drop the
|
|
686
|
-
// promise. Wire as a `.catch` so an unhandled-rejection never
|
|
687
|
-
// crashes the engine loop.
|
|
688
|
-
void registry
|
|
689
|
-
.add({
|
|
690
|
-
id: jobId,
|
|
691
|
-
pid: child.pid ?? -1,
|
|
692
|
-
command: cmd,
|
|
693
|
-
bashClass: classification.class,
|
|
694
|
-
cwd: startCwd,
|
|
695
|
-
sessionId: ctx.session.id,
|
|
696
|
-
})
|
|
697
|
-
.catch(() => {
|
|
698
|
-
// Best-effort persistence; the in-process tool still returned
|
|
699
|
-
// the jobId so the engine loop knows the spawn succeeded.
|
|
700
|
-
});
|
|
701
|
-
emitEvent(ctx.session, 'bash.background_started', {
|
|
702
|
-
jobId,
|
|
703
|
-
pid: child.pid ?? -1,
|
|
704
|
-
cmd,
|
|
705
|
-
});
|
|
706
|
-
recordToolResult(ctx.session, toolCallId, 'success', `bash background jobId=${jobId} pid=${child.pid ?? -1}`);
|
|
707
|
-
return {
|
|
708
|
-
stdout: `bash started in background as ${jobId}`,
|
|
709
|
-
stderr: '',
|
|
710
|
-
exitCode: 0,
|
|
711
|
-
jobId,
|
|
712
|
-
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
713
|
-
truncated: false,
|
|
714
|
-
timedOut: false,
|
|
715
|
-
cancelled: false,
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Legacy export preserved for callers / tests. Delegates to the
|
|
720
|
-
* new JobRegistry and projects entries back into the historical
|
|
721
|
-
* `PugiJob` shape.
|
|
722
|
-
*/
|
|
723
|
-
export function listJobs() {
|
|
724
|
-
const entries = readRegistryEntriesSync();
|
|
725
|
-
return entries.map(entryToLegacyJob);
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Legacy export preserved for callers / tests. Delegates to the
|
|
729
|
-
* new JobRegistry. Returns the same `{ killed, reason? }` shape so the
|
|
730
|
-
* existing bash-tool test suite continues to pass without an
|
|
731
|
-
* end-to-end rewrite.
|
|
732
|
-
*/
|
|
733
|
-
export function killJob(jobId) {
|
|
734
|
-
const entries = readRegistryEntriesSync();
|
|
735
|
-
const target = entries.find((entry) => entry.id === jobId);
|
|
736
|
-
if (!target)
|
|
737
|
-
return { killed: false, reason: `unknown jobId: ${jobId}` };
|
|
738
|
-
// Mirror the legacy semantics: synchronous SIGTERM + best-effort
|
|
739
|
-
// SIGKILL escalation + remove the entry from the ledger so the
|
|
740
|
-
// bash-tool test suite's `listJobs().find(...) === undefined`
|
|
741
|
-
// assertion keeps holding. The richer `JobRegistry.kill` (status
|
|
742
|
-
// transitions, async exit polling) is what the new `pugi jobs kill`
|
|
743
|
-
// CLI command uses.
|
|
744
|
-
try {
|
|
745
|
-
process.kill(target.pid, 'SIGTERM');
|
|
746
|
-
}
|
|
747
|
-
catch (error) {
|
|
748
|
-
const code = error.code;
|
|
749
|
-
if (code === 'ESRCH') {
|
|
750
|
-
removeRegistryEntrySync(jobId);
|
|
751
|
-
return { killed: false, reason: 'job already exited' };
|
|
752
|
-
}
|
|
753
|
-
return { killed: false, reason: `kill failed: ${error.message}` };
|
|
754
|
-
}
|
|
755
|
-
setTimeout(() => {
|
|
756
|
-
try {
|
|
757
|
-
process.kill(target.pid, 0);
|
|
758
|
-
try {
|
|
759
|
-
process.kill(target.pid, 'SIGKILL');
|
|
760
|
-
}
|
|
761
|
-
catch {
|
|
762
|
-
// gone between the check and the signal
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
catch {
|
|
766
|
-
// already dead
|
|
767
|
-
}
|
|
768
|
-
}, BASH_SIGKILL_GRACE_MS).unref();
|
|
769
|
-
removeRegistryEntrySync(jobId);
|
|
770
|
-
return { killed: true };
|
|
771
|
-
}
|
|
772
|
-
function entryToLegacyJob(entry) {
|
|
773
|
-
return {
|
|
774
|
-
jobId: entry.id,
|
|
775
|
-
pid: entry.pid,
|
|
776
|
-
cwd: entry.cwd,
|
|
777
|
-
cmd: entry.command,
|
|
778
|
-
class: entry.bashClass,
|
|
779
|
-
startedAt: entry.startedAt,
|
|
780
|
-
sessionId: entry.sessionId,
|
|
781
|
-
};
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Synchronous read of the registry file. Used by the legacy
|
|
785
|
-
* `listJobs()` / `killJob()` exports because they cannot return a
|
|
786
|
-
* promise without a breaking signature change. The new `JobRegistry`
|
|
787
|
-
* interface is the async path.
|
|
788
|
-
*/
|
|
789
|
-
function readRegistryEntriesSync() {
|
|
790
|
-
const path = join(homedir(), '.pugi', 'jobs.json');
|
|
791
|
-
// Inline read so the legacy listJobs/killJob entry points do not
|
|
792
|
-
// require an async hop into JobRegistry. The shape parsing mirrors
|
|
793
|
-
// `normalizeEntry` inside `core/jobs/registry.ts`.
|
|
794
|
-
if (!existsSync(path))
|
|
795
|
-
return [];
|
|
796
|
-
try {
|
|
797
|
-
const raw = readFileSync(path, 'utf8');
|
|
798
|
-
if (raw.trim() === '')
|
|
799
|
-
return [];
|
|
800
|
-
const parsed = JSON.parse(raw);
|
|
801
|
-
if (!Array.isArray(parsed))
|
|
802
|
-
return [];
|
|
803
|
-
const out = [];
|
|
804
|
-
for (const candidate of parsed) {
|
|
805
|
-
if (typeof candidate !== 'object' || candidate === null)
|
|
806
|
-
continue;
|
|
807
|
-
const c = candidate;
|
|
808
|
-
const id = typeof c['id'] === 'string'
|
|
809
|
-
? c['id']
|
|
810
|
-
: typeof c['jobId'] === 'string'
|
|
811
|
-
? c['jobId']
|
|
812
|
-
: undefined;
|
|
813
|
-
const pid = typeof c['pid'] === 'number' ? c['pid'] : undefined;
|
|
814
|
-
const command = typeof c['command'] === 'string'
|
|
815
|
-
? c['command']
|
|
816
|
-
: typeof c['cmd'] === 'string'
|
|
817
|
-
? c['cmd']
|
|
818
|
-
: undefined;
|
|
819
|
-
if (!id || pid === undefined || command === undefined)
|
|
820
|
-
continue;
|
|
821
|
-
const bashClassRaw = typeof c['bashClass'] === 'string'
|
|
822
|
-
? c['bashClass']
|
|
823
|
-
: typeof c['class'] === 'string'
|
|
824
|
-
? c['class']
|
|
825
|
-
: 'unknown';
|
|
826
|
-
const status = c['status'] === 'finished' ||
|
|
827
|
-
c['status'] === 'killed' ||
|
|
828
|
-
c['status'] === 'failed' ||
|
|
829
|
-
c['status'] === 'abandoned'
|
|
830
|
-
? c['status']
|
|
831
|
-
: 'running';
|
|
832
|
-
out.push({
|
|
833
|
-
id,
|
|
834
|
-
pid,
|
|
835
|
-
command,
|
|
836
|
-
bashClass: bashClassRaw,
|
|
837
|
-
cwd: typeof c['cwd'] === 'string' ? c['cwd'] : '',
|
|
838
|
-
startedAt: typeof c['startedAt'] === 'string'
|
|
839
|
-
? c['startedAt']
|
|
840
|
-
: new Date().toISOString(),
|
|
841
|
-
status,
|
|
842
|
-
sessionId: typeof c['sessionId'] === 'string' ? c['sessionId'] : 'unknown',
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
return out;
|
|
846
|
-
}
|
|
847
|
-
catch {
|
|
848
|
-
return [];
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* Workspace-git-boundary guard (CEO P0 #51).
|
|
853
|
-
*
|
|
854
|
-
* Background: CEO live REPL surfaced a scenario where the customer
|
|
855
|
-
* workspace dir was created INSIDE another git repository (the Pugi
|
|
856
|
-
* monorepo itself). The model emitted `git init && git add . && git
|
|
857
|
-
* commit -m ...` against that workspace. The workspace had no `.git`
|
|
858
|
-
* of its own so git silently walked up to the outer repo's `.git` and
|
|
859
|
-
* committed the customer's files directly to the monorepo's main
|
|
860
|
-
* branch. Had the outer remote been FF-permissive, those files would
|
|
861
|
-
* have pushed to production. This is a customer-of-customer leak.
|
|
862
|
-
*
|
|
863
|
-
* The guard: when the agent emits a mutating git op (add / commit /
|
|
864
|
-
* push / rebase / reset / checkout) and the effective git toplevel
|
|
865
|
-
* (`git -C $cwd rev-parse --show-toplevel`) sits OUTSIDE the workspace
|
|
866
|
-
* root, block the command. The model is steered (via the persona
|
|
867
|
-
* prompt) to run `git init` first; the guard is the defensive net so
|
|
868
|
-
* a careless model emission cannot cross the boundary.
|
|
869
|
-
*
|
|
870
|
-
* Exported so the spec can exercise the predicate in isolation without
|
|
871
|
-
* having to drive the whole bash tool.
|
|
872
|
-
*/
|
|
873
|
-
export const GIT_BOUNDARY_BLOCK_PREFIX = 'git boundary escape:';
|
|
874
|
-
/**
|
|
875
|
-
* Subcommands we treat as definitely mutating for the boundary check.
|
|
876
|
-
* We intentionally OMIT subcommands that have common read-only modes
|
|
877
|
-
* (`branch --list`, `tag --list`, `stash list`, `remote -v`) to keep
|
|
878
|
-
* the guard precise. The CEO P0 #51 leak vector is files written to
|
|
879
|
-
* an ancestor repo's working tree / refs, which the included set
|
|
880
|
-
* fully covers. The omitted subcommands can still create refs in the
|
|
881
|
-
* outer .git, but they do not move customer files into the outer
|
|
882
|
-
* repo's commit graph, so the leak severity is lower and the
|
|
883
|
-
* ergonomic cost of false positives on `--list` flags is higher.
|
|
884
|
-
*/
|
|
885
|
-
const MUTATING_GIT_SUBCOMMANDS = new Set([
|
|
886
|
-
'add',
|
|
887
|
-
'commit',
|
|
888
|
-
'push',
|
|
889
|
-
'rebase',
|
|
890
|
-
'reset',
|
|
891
|
-
'checkout',
|
|
892
|
-
'merge',
|
|
893
|
-
'restore',
|
|
894
|
-
'switch',
|
|
895
|
-
'cherry-pick',
|
|
896
|
-
'am',
|
|
897
|
-
'apply',
|
|
898
|
-
'clean',
|
|
899
|
-
'rm',
|
|
900
|
-
'mv',
|
|
901
|
-
]);
|
|
902
|
-
/**
|
|
903
|
-
* Inspect a shell command for mutating git operations. Returns the
|
|
904
|
-
* first matching subcommand (e.g. 'commit') or null when none of the
|
|
905
|
-
* components are mutating git ops.
|
|
906
|
-
*
|
|
907
|
-
* We split on `&&`, `||`, `;`, `|` so a compound like
|
|
908
|
-
* `mkdir foo && cd foo && git add .` is correctly flagged on the
|
|
909
|
-
* trailing git component.
|
|
910
|
-
*/
|
|
911
|
-
export function detectMutatingGitOp(cmd) {
|
|
912
|
-
const components = cmd.split(/\s*(?:&&|\|\||;|\|)\s*/);
|
|
913
|
-
for (const raw of components) {
|
|
914
|
-
const component = raw.trim();
|
|
915
|
-
if (component === '')
|
|
916
|
-
continue;
|
|
917
|
-
// Strip leading `sudo` wrapper which would otherwise hide the verb.
|
|
918
|
-
const stripped = component.replace(/^sudo\s+/, '');
|
|
919
|
-
// Match `git [<global-flags>] <subcommand> ...`. Global flags we
|
|
920
|
-
// tolerate:
|
|
921
|
-
// - long flag: `--no-pager`, `--git-dir=.git`
|
|
922
|
-
// - short flag with attached value: `-C <path>`, `-c <k=v>`
|
|
923
|
-
// - bare short flag: `-P`
|
|
924
|
-
// Anything weirder falls through and the predicate returns null,
|
|
925
|
-
// which means the guard does not fire on that component — safer
|
|
926
|
-
// to err open here because the destructive classifier and the
|
|
927
|
-
// outer permission gate are independent defences.
|
|
928
|
-
const match = stripped.match(/^git(?:\s+(?:--[A-Za-z][A-Za-z0-9-]*(?:=\S+)?|-[CcP](?:\s+\S+)?|-[A-Za-z]+))*\s+([a-z][a-z0-9-]*)\b/);
|
|
929
|
-
if (!match)
|
|
930
|
-
continue;
|
|
931
|
-
const subcommand = match[1];
|
|
932
|
-
if (subcommand && MUTATING_GIT_SUBCOMMANDS.has(subcommand)) {
|
|
933
|
-
return subcommand;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
return null;
|
|
937
|
-
}
|
|
938
|
-
/**
|
|
939
|
-
* Resolve the workspace's effective git boundary. Returns:
|
|
940
|
-
* - the absolute path of the .git toplevel that owns `cwd`
|
|
941
|
-
* - null when no .git ancestor exists at all (standalone, no repo)
|
|
942
|
-
*
|
|
943
|
-
* Pure filesystem walk so the guard does not depend on git being on
|
|
944
|
-
* PATH. We look for either a `.git` directory or a `.git` file (the
|
|
945
|
-
* worktree case where `.git` is a pointer file).
|
|
946
|
-
*/
|
|
947
|
-
export function resolveGitToplevel(cwd) {
|
|
948
|
-
let dir = cwd;
|
|
949
|
-
while (true) {
|
|
950
|
-
const dotGit = join(dir, '.git');
|
|
951
|
-
if (existsSync(dotGit))
|
|
952
|
-
return dir;
|
|
953
|
-
const parent = resolve(dir, '..');
|
|
954
|
-
if (parent === dir)
|
|
955
|
-
return null;
|
|
956
|
-
dir = parent;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
/**
|
|
960
|
-
* The actual guard. Returns null when the command is allowed; returns
|
|
961
|
-
* a block descriptor when it should be denied. The block message uses
|
|
962
|
-
* the literal prefix `git boundary escape:` so callers (and the spec)
|
|
963
|
-
* can pattern-match.
|
|
964
|
-
*/
|
|
965
|
-
export function enforceGitBoundary(cmd, startCwd, workspaceRoot) {
|
|
966
|
-
const subcommand = detectMutatingGitOp(cmd);
|
|
967
|
-
if (subcommand === null)
|
|
968
|
-
return null;
|
|
969
|
-
// Resolve symlinks on both sides so a /var → /private/var macOS
|
|
970
|
-
// realpath divergence does not produce a false escape.
|
|
971
|
-
const root = safeRealpath(workspaceRoot);
|
|
972
|
-
const toplevel = resolveGitToplevel(safeRealpath(startCwd));
|
|
973
|
-
const resolvedToplevel = toplevel === null ? null : safeRealpath(toplevel);
|
|
974
|
-
if (resolvedToplevel === root)
|
|
975
|
-
return null;
|
|
976
|
-
// Either no .git anywhere (standalone) OR the .git that wins is an
|
|
977
|
-
// ancestor — both are escape scenarios. Operator must run `git init`
|
|
978
|
-
// explicitly inside the workspace.
|
|
979
|
-
if (resolvedToplevel === null) {
|
|
980
|
-
return {
|
|
981
|
-
subcommand,
|
|
982
|
-
resolvedToplevel: null,
|
|
983
|
-
reason: `${GIT_BOUNDARY_BLOCK_PREFIX} workspace root '${workspaceRoot}' has no .git ` +
|
|
984
|
-
`and no ancestor repository exists. Run \`git init\` in the workspace first ` +
|
|
985
|
-
`before \`git ${subcommand}\`.`,
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
return {
|
|
989
|
-
subcommand,
|
|
990
|
-
resolvedToplevel,
|
|
991
|
-
reason: `${GIT_BOUNDARY_BLOCK_PREFIX} workspace root '${workspaceRoot}' has no .git; ` +
|
|
992
|
-
`outer toplevel is '${resolvedToplevel}'. Run \`git init\` in the workspace ` +
|
|
993
|
-
`first before \`git ${subcommand}\` (otherwise the operation would write to ` +
|
|
994
|
-
`the ancestor repository, not the workspace).`,
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
function safeRealpath(path) {
|
|
998
|
-
try {
|
|
999
|
-
return realpathSync(path);
|
|
1000
|
-
}
|
|
1001
|
-
catch {
|
|
1002
|
-
return path;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
function removeRegistryEntrySync(jobId) {
|
|
1006
|
-
const path = join(homedir(), '.pugi', 'jobs.json');
|
|
1007
|
-
const entries = readRegistryEntriesSync().filter((entry) => entry.id !== jobId);
|
|
1008
|
-
try {
|
|
1009
|
-
mkdirSync(join(homedir(), '.pugi'), { recursive: true });
|
|
1010
|
-
writeFileSync(path, `${JSON.stringify(entries, null, 2)}\n`, {
|
|
1011
|
-
encoding: 'utf8',
|
|
1012
|
-
mode: 0o600,
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
catch {
|
|
1016
|
-
// best-effort
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Synchronous helper used by the legacy tool-bridge path. It wraps
|
|
1021
|
-
* `spawnSync` for the simplest case (no background, no overflow
|
|
1022
|
-
* artifact, default timeout) so callers that cannot await a promise
|
|
1023
|
-
* still get the class-aware permission gate. Returns the same shape
|
|
1024
|
-
* as the async tool minus the cwd carry-over (since spawnSync
|
|
1025
|
-
* cannot stream we approximate the cap by post-truncation).
|
|
1026
|
-
*/
|
|
1027
|
-
export function bashToolSync(input, ctx) {
|
|
1028
|
-
const cmd = input.cmd ?? '';
|
|
1029
|
-
const additionalDirectories = ctx.additionalDirectories ?? [];
|
|
1030
|
-
const source = ctx.source ?? 'agent';
|
|
1031
|
-
const toolCallId = recordToolCall(ctx.session, 'bash', cmd);
|
|
1032
|
-
const startCwd = resolveStartCwd(input.cwd ?? ctx.lastBashCwd, ctx.root, additionalDirectories);
|
|
1033
|
-
// Workspace-git-boundary guard (CEO P0 #51). Fires
|
|
1034
|
-
// BEFORE the permission gate so the structural boundary diagnostic
|
|
1035
|
-
// is the one the operator sees. See the async path for the full
|
|
1036
|
-
// rationale.
|
|
1037
|
-
const boundaryBlock = enforceGitBoundary(cmd, startCwd, ctx.root);
|
|
1038
|
-
if (boundaryBlock !== null) {
|
|
1039
|
-
emitEvent(ctx.session, 'bash.git_boundary_escape', {
|
|
1040
|
-
cmd,
|
|
1041
|
-
workspaceRoot: ctx.root,
|
|
1042
|
-
resolvedToplevel: boundaryBlock.resolvedToplevel ?? null,
|
|
1043
|
-
});
|
|
1044
|
-
recordToolResult(ctx.session, toolCallId, 'error', boundaryBlock.reason);
|
|
1045
|
-
return {
|
|
1046
|
-
stdout: '',
|
|
1047
|
-
stderr: boundaryBlock.reason,
|
|
1048
|
-
exitCode: 126,
|
|
1049
|
-
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
1050
|
-
truncated: false,
|
|
1051
|
-
timedOut: false,
|
|
1052
|
-
cancelled: false,
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
|
-
const decision = evaluateBashPermission(cmd, ctx.settings.permissions.mode, {
|
|
1056
|
-
workspaceRoot: ctx.root,
|
|
1057
|
-
additionalDirectories,
|
|
1058
|
-
source,
|
|
1059
|
-
});
|
|
1060
|
-
if (decision.decision !== 'allow') {
|
|
1061
|
-
const reason = `Permission ${decision.decision}: ${decision.reason}`;
|
|
1062
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
1063
|
-
// #21: mirror the async-path emission so sync callers
|
|
1064
|
-
// (spawnSync fallback) produce the same tenant-wide audit trail.
|
|
1065
|
-
writeAuditEvent({
|
|
1066
|
-
event: 'permission_denied',
|
|
1067
|
-
sessionId: ctx.session.id,
|
|
1068
|
-
workspaceRoot: ctx.root,
|
|
1069
|
-
data: {
|
|
1070
|
-
tool: 'bash',
|
|
1071
|
-
source,
|
|
1072
|
-
decision: decision.decision,
|
|
1073
|
-
reason: decision.reason,
|
|
1074
|
-
cmdPreview: cmd.slice(0, 200),
|
|
1075
|
-
},
|
|
1076
|
-
});
|
|
1077
|
-
return {
|
|
1078
|
-
stdout: '',
|
|
1079
|
-
stderr: `Permission denied: ${decision.reason}`,
|
|
1080
|
-
exitCode: 126,
|
|
1081
|
-
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
1082
|
-
truncated: false,
|
|
1083
|
-
timedOut: false,
|
|
1084
|
-
cancelled: false,
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
// CEO P1 #25 — sync path observes pre-spawn cancellation too. The
|
|
1088
|
-
// sync path is used by the engine-loop tool-bridge (`bashToolSync`
|
|
1089
|
-
// from tool-bridge.ts:1385); we cannot mid-stream cancel that path
|
|
1090
|
-
// without rewriting spawnSync, but the pre-spawn gate still gives
|
|
1091
|
-
// the operator a quick-exit window between permission and shell
|
|
1092
|
-
// launch.
|
|
1093
|
-
if (ctx.cancellation?.isAborted === true) {
|
|
1094
|
-
const reason = 'operator_aborted: bash refused before spawn';
|
|
1095
|
-
emitEvent(ctx.session, 'bash.cancelled', { cmd, phase: 'pre_spawn_sync' });
|
|
1096
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
|
|
1097
|
-
return {
|
|
1098
|
-
stdout: '',
|
|
1099
|
-
stderr: reason,
|
|
1100
|
-
exitCode: 130,
|
|
1101
|
-
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
1102
|
-
truncated: false,
|
|
1103
|
-
timedOut: false,
|
|
1104
|
-
cancelled: true,
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
const timeoutMs = sanitizeTimeout(input.timeoutMs);
|
|
1108
|
-
const childEnv = buildChildEnv();
|
|
1109
|
-
// Pugi backlog P2 — redirect path for the sync entry. The
|
|
1110
|
-
// engine loop's tool-bridge dispatches through `bashToolSync`, so
|
|
1111
|
-
// the redirect contract has to be honoured here too — otherwise a
|
|
1112
|
-
// model that asks for log discipline through the bash tool surface
|
|
1113
|
-
// would get its stdout buffered + truncated through the legacy
|
|
1114
|
-
// pipeline. `spawnSync` accepts file descriptors in `stdio` so we
|
|
1115
|
-
// can hand the log fd in directly, same as the async path.
|
|
1116
|
-
let redirectState = null;
|
|
1117
|
-
if (input.redirect !== undefined) {
|
|
1118
|
-
try {
|
|
1119
|
-
const target = resolveRedirectTarget({
|
|
1120
|
-
workspaceRoot: ctx.root,
|
|
1121
|
-
sessionId: ctx.session.id,
|
|
1122
|
-
toolCallId,
|
|
1123
|
-
command: cmd,
|
|
1124
|
-
override: input.redirect.path,
|
|
1125
|
-
});
|
|
1126
|
-
const { fd, tempPath } = openRedirectFile(target);
|
|
1127
|
-
redirectState = {
|
|
1128
|
-
target,
|
|
1129
|
-
fd,
|
|
1130
|
-
tempPath,
|
|
1131
|
-
tailLines: normalizeTailLines(input.redirect.tailLines),
|
|
1132
|
-
};
|
|
1133
|
-
}
|
|
1134
|
-
catch (error) {
|
|
1135
|
-
const reason = `redirect refused: ${error.message}`;
|
|
1136
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
1137
|
-
return {
|
|
1138
|
-
stdout: '',
|
|
1139
|
-
stderr: reason,
|
|
1140
|
-
exitCode: 126,
|
|
1141
|
-
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
1142
|
-
truncated: false,
|
|
1143
|
-
timedOut: false,
|
|
1144
|
-
cancelled: false,
|
|
1145
|
-
};
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
const stdioLayout = redirectState !== null
|
|
1149
|
-
? ['ignore', redirectState.fd, redirectState.fd]
|
|
1150
|
-
: ['ignore', 'pipe', 'pipe'];
|
|
1151
|
-
const result = spawnSync('/bin/sh', ['-c', cmd], {
|
|
1152
|
-
cwd: startCwd,
|
|
1153
|
-
env: childEnv,
|
|
1154
|
-
encoding: 'utf8',
|
|
1155
|
-
stdio: stdioLayout,
|
|
1156
|
-
timeout: timeoutMs,
|
|
1157
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
1158
|
-
});
|
|
1159
|
-
const timedOut = result.error?.code === 'ETIMEDOUT' ||
|
|
1160
|
-
result.signal === 'SIGTERM';
|
|
1161
|
-
const nextCwd = computeNextCwd(cmd, startCwd, ctx.root, additionalDirectories, ctx.session);
|
|
1162
|
-
// Redirect short-circuit before the buffered-path artifact logic.
|
|
1163
|
-
// We close our copy of the fd before rename so the inode is no
|
|
1164
|
-
// longer held open by the parent process.
|
|
1165
|
-
if (redirectState !== null) {
|
|
1166
|
-
try {
|
|
1167
|
-
closeSync(redirectState.fd);
|
|
1168
|
-
}
|
|
1169
|
-
catch {
|
|
1170
|
-
// already closed
|
|
1171
|
-
}
|
|
1172
|
-
try {
|
|
1173
|
-
finaliseRedirectFile(redirectState.target, redirectState.tempPath);
|
|
1174
|
-
}
|
|
1175
|
-
catch {
|
|
1176
|
-
// best-effort
|
|
1177
|
-
}
|
|
1178
|
-
const redirectExitCode = timedOut ? 124 : result.status ?? 1;
|
|
1179
|
-
const envelope = applyRedirect({
|
|
1180
|
-
target: redirectState.target,
|
|
1181
|
-
exitCode: redirectExitCode,
|
|
1182
|
-
tailLines: redirectState.tailLines,
|
|
1183
|
-
});
|
|
1184
|
-
if (timedOut) {
|
|
1185
|
-
emitEvent(ctx.session, 'bash.timeout', { cmd, timeoutMs });
|
|
1186
|
-
recordToolResult(ctx.session, toolCallId, 'error', `bash timed out after ${timeoutMs}ms (redirect=${envelope.logPath})`);
|
|
1187
|
-
}
|
|
1188
|
-
else {
|
|
1189
|
-
recordToolResult(ctx.session, toolCallId, 'success', `bash exit=${redirectExitCode} redirect=${envelope.logPath}`);
|
|
1190
|
-
}
|
|
1191
|
-
return {
|
|
1192
|
-
stdout: envelope.stdout,
|
|
1193
|
-
stderr: envelope.stderr,
|
|
1194
|
-
exitCode: redirectExitCode,
|
|
1195
|
-
nextCwd,
|
|
1196
|
-
truncated: envelope.truncated,
|
|
1197
|
-
timedOut,
|
|
1198
|
-
cancelled: false,
|
|
1199
|
-
logPath: envelope.logPath,
|
|
1200
|
-
tail: envelope.tail,
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
const stdoutFull = (result.stdout ?? '').toString();
|
|
1204
|
-
const stderrFull = (result.stderr ?? '').toString();
|
|
1205
|
-
const truncated = stdoutFull.length + stderrFull.length > BASH_OUTPUT_CAP_BYTES;
|
|
1206
|
-
let artifactRef;
|
|
1207
|
-
let stdoutOut = stdoutFull;
|
|
1208
|
-
let stderrOut = stderrFull;
|
|
1209
|
-
if (truncated) {
|
|
1210
|
-
artifactRef = persistOverflow({
|
|
1211
|
-
root: ctx.root,
|
|
1212
|
-
sessionId: ctx.session.id,
|
|
1213
|
-
toolCallId,
|
|
1214
|
-
stdout: stdoutFull,
|
|
1215
|
-
stderr: stderrFull,
|
|
1216
|
-
});
|
|
1217
|
-
({ stdout: stdoutOut, stderr: stderrOut } = capToCombined(stdoutFull, stderrFull));
|
|
1218
|
-
}
|
|
1219
|
-
const exitCode = timedOut ? 124 : result.status ?? 1;
|
|
1220
|
-
if (timedOut) {
|
|
1221
|
-
emitEvent(ctx.session, 'bash.timeout', { cmd, timeoutMs });
|
|
1222
|
-
recordToolResult(ctx.session, toolCallId, 'error', `bash timed out after ${timeoutMs}ms`);
|
|
1223
|
-
}
|
|
1224
|
-
else {
|
|
1225
|
-
recordToolResult(ctx.session, toolCallId, 'success', `bash exit=${exitCode} bytes=${stdoutFull.length + stderrFull.length}${artifactRef ? ` overflow=${artifactRef}` : ''}`);
|
|
1226
|
-
}
|
|
1227
|
-
return {
|
|
1228
|
-
stdout: stdoutOut,
|
|
1229
|
-
stderr: stderrOut,
|
|
1230
|
-
exitCode,
|
|
1231
|
-
artifactRef,
|
|
1232
|
-
nextCwd,
|
|
1233
|
-
truncated,
|
|
1234
|
-
timedOut,
|
|
1235
|
-
cancelled: false,
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
//# sourceMappingURL=bash.js.map
|