@agentwhy/cli 0.0.0-dev
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 +202 -0
- package/NOTICE +2 -0
- package/README.md +181 -0
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/dist/adapter/claude-code/contract/assumptions.js +208 -0
- package/dist/adapter/claude-code/contract/denials.js +15 -0
- package/dist/adapter/claude-code/contract/entry-points.js +15 -0
- package/dist/adapter/claude-code/contract/fields.js +73 -0
- package/dist/adapter/claude-code/contract/hooks.js +96 -0
- package/dist/adapter/claude-code/contract/identifiers.js +15 -0
- package/dist/adapter/claude-code/contract/layout.js +29 -0
- package/dist/adapter/claude-code/contract/line-types.js +26 -0
- package/dist/adapter/claude-code/contract/projects.js +23 -0
- package/dist/adapter/claude-code/contract/recognition.js +14 -0
- package/dist/adapter/claude-code/contract/session-title.js +20 -0
- package/dist/adapter/claude-code/contract/settings.js +27 -0
- package/dist/adapter/claude-code/contract/task-notifications.js +33 -0
- package/dist/adapter/claude-code/contract/tools.js +37 -0
- package/dist/adapter/claude-code/contract/version.js +51 -0
- package/dist/adapter/claude-code/discovery/claude-code-project-catalogue.js +129 -0
- package/dist/adapter/claude-code/discovery/claude-code-recognition.js +46 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-catalogue.js +100 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-discovery.js +61 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-titles.js +28 -0
- package/dist/adapter/claude-code/discovery/discovered-session.js +1 -0
- package/dist/adapter/claude-code/discovery/session-entries.js +41 -0
- package/dist/adapter/claude-code/discovery/transcript-tail.js +81 -0
- package/dist/adapter/claude-code/events/claude-code-session-source.js +197 -0
- package/dist/adapter/claude-code/events/delegation-index.js +15 -0
- package/dist/adapter/claude-code/events/transcript-scan.js +179 -0
- package/dist/adapter/claude-code/hooks/hook-output.js +42 -0
- package/dist/adapter/claude-code/hooks/pre-tool-use-input.js +23 -0
- package/dist/adapter/claude-code/hooks/stop-input.js +25 -0
- package/dist/adapter/claude-code/hooks/subagent-stop-input.js +26 -0
- package/dist/adapter/claude-code/policy/deny-rules.js +46 -0
- package/dist/adapter/claude-code/probe/claude-code-probe.js +86 -0
- package/dist/adapter/claude-code/probe/collectors/agent-tool-input-key-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/denial-kind-collector.js +20 -0
- package/dist/adapter/claude-code/probe/collectors/denied-call-collector.js +11 -0
- package/dist/adapter/claude-code/probe/collectors/identifier-collector.js +19 -0
- package/dist/adapter/claude-code/probe/collectors/line-collector.js +1 -0
- package/dist/adapter/claude-code/probe/collectors/line-type-collector.js +29 -0
- package/dist/adapter/claude-code/probe/collectors/sidechain-collector.js +20 -0
- package/dist/adapter/claude-code/probe/collectors/task-notification-collector.js +51 -0
- package/dist/adapter/claude-code/probe/collectors/tool-result-reference-collector.js +31 -0
- package/dist/adapter/claude-code/probe/collectors/tool-use-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/tool-version-collector.js +14 -0
- package/dist/adapter/claude-code/probe/collectors/top-level-key-collector.js +14 -0
- package/dist/adapter/claude-code/probe/collectors/unknown-line-type-key-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/working-directory-collector.js +28 -0
- package/dist/adapter/claude-code/probe/doctor-report.js +1 -0
- package/dist/adapter/claude-code/probe/meta-tally.js +42 -0
- package/dist/adapter/claude-code/probe/session-probe.js +1 -0
- package/dist/adapter/claude-code/probe/session-tally.js +41 -0
- package/dist/adapter/claude-code/probe/transcript-line.js +30 -0
- package/dist/adapter/claude-code/probe/transcript-tally.js +74 -0
- package/dist/adapter/claude-code/settings/deny-entries.js +65 -0
- package/dist/adapter/claude-code/settings/hook-entries.js +303 -0
- package/dist/adapter/codex/contract/actions.js +69 -0
- package/dist/adapter/codex/contract/assumptions.js +55 -0
- package/dist/adapter/codex/contract/capabilities.js +24 -0
- package/dist/adapter/codex/contract/delegations.js +30 -0
- package/dist/adapter/codex/contract/deliveries.js +25 -0
- package/dist/adapter/codex/contract/envelope.js +33 -0
- package/dist/adapter/codex/contract/hook-refusals.js +14 -0
- package/dist/adapter/codex/contract/hooks.js +46 -0
- package/dist/adapter/codex/contract/messages.js +47 -0
- package/dist/adapter/codex/contract/permissions.js +27 -0
- package/dist/adapter/codex/contract/reviews.js +13 -0
- package/dist/adapter/codex/contract/session.js +31 -0
- package/dist/adapter/codex/contract/turns.js +23 -0
- package/dist/adapter/codex/contract/version.js +15 -0
- package/dist/adapter/codex/discovery/codex-conversations.js +44 -0
- package/dist/adapter/codex/discovery/codex-project-catalogue.js +49 -0
- package/dist/adapter/codex/discovery/codex-recognition.js +20 -0
- package/dist/adapter/codex/discovery/codex-session-catalogue.js +28 -0
- package/dist/adapter/codex/discovery/codex-session-discovery.js +115 -0
- package/dist/adapter/codex/discovery/codex-session-index.js +21 -0
- package/dist/adapter/codex/discovery/codex-session-titles.js +135 -0
- package/dist/adapter/codex/discovery/session-header.js +86 -0
- package/dist/adapter/codex/discovery/session-owners.js +11 -0
- package/dist/adapter/codex/discovery/session-roots.js +49 -0
- package/dist/adapter/codex/events/action-items.js +138 -0
- package/dist/adapter/codex/events/capability-records.js +26 -0
- package/dist/adapter/codex/events/codex-session-source.js +121 -0
- package/dist/adapter/codex/events/hook-refusals.js +27 -0
- package/dist/adapter/codex/events/rollout-scan.js +614 -0
- package/dist/adapter/codex/events/turn-permissions.js +60 -0
- package/dist/adapter/codex/hooks/pre-tool-use-input.js +24 -0
- package/dist/adapter/codex/hooks/stop-refusals.js +57 -0
- package/dist/adapter/codex/probe/codex-doctor-report.js +1 -0
- package/dist/adapter/codex/probe/codex-probe.js +268 -0
- package/dist/adapter/codex/settings/codex-hooks.js +83 -0
- package/dist/cli/cli-command.js +1 -0
- package/dist/cli/command-router.js +42 -0
- package/dist/cli/commands/check-cli-command.js +128 -0
- package/dist/cli/commands/codex-stop-cli-command.js +27 -0
- package/dist/cli/commands/doctor-arguments.js +32 -0
- package/dist/cli/commands/doctor-cli-command.js +31 -0
- package/dist/cli/commands/doctor-usage.js +14 -0
- package/dist/cli/commands/init-cli-command.js +123 -0
- package/dist/cli/commands/menu-cli-command.js +58 -0
- package/dist/cli/commands/notify-cli-command.js +158 -0
- package/dist/cli/commands/refuse-cli-command.js +79 -0
- package/dist/cli/commands/report-arguments.js +62 -0
- package/dist/cli/commands/report-cli-command.js +36 -0
- package/dist/cli/commands/report-usage.js +41 -0
- package/dist/cli/commands/sessions-cli-command.js +184 -0
- package/dist/cli/commands/start-cli-command.js +83 -0
- package/dist/cli/commands/watch-cli-command.js +142 -0
- package/dist/cli/exit-codes.js +13 -0
- package/dist/cli/project-switches.js +46 -0
- package/dist/cli.js +80 -0
- package/dist/composition-root.js +411 -0
- package/dist/core/access/command-line.js +454 -0
- package/dist/core/access/listing.js +58 -0
- package/dist/core/access/path-shape.js +59 -0
- package/dist/core/access/path-tokens.js +37 -0
- package/dist/core/access/protected-access.js +310 -0
- package/dist/core/access/protected-values.js +102 -0
- package/dist/core/access/recorded-effect.js +67 -0
- package/dist/core/access/returns.js +173 -0
- package/dist/core/access/search-output.js +34 -0
- package/dist/core/access/search-reach.js +268 -0
- package/dist/core/access/uses.js +143 -0
- package/dist/core/access/words-before.js +52 -0
- package/dist/core/agent.js +1 -0
- package/dist/core/capability.js +11 -0
- package/dist/core/combined-project-catalogue.js +36 -0
- package/dist/core/combined-session-catalogue.js +25 -0
- package/dist/core/completeness.js +10 -0
- package/dist/core/context.js +1 -0
- package/dist/core/correlation/correlate.js +329 -0
- package/dist/core/delegation.js +1 -0
- package/dist/core/detector/prompt-signals.js +78 -0
- package/dist/core/entry-point.js +1 -0
- package/dist/core/event.js +1 -0
- package/dist/core/evidence.js +9 -0
- package/dist/core/late-message.js +32 -0
- package/dist/core/message.js +1 -0
- package/dist/core/nearest-project.js +28 -0
- package/dist/core/policy/default-policy.js +32 -0
- package/dist/core/policy/glob.js +56 -0
- package/dist/core/policy/parse-policy.js +70 -0
- package/dist/core/policy/policy.js +20 -0
- package/dist/core/policy/resolve-policy.js +16 -0
- package/dist/core/project-catalogue.js +1 -0
- package/dist/core/project-root.js +133 -0
- package/dist/core/provider-titles.js +10 -0
- package/dist/core/redaction/entropy.js +46 -0
- package/dist/core/redaction/redacted.js +4 -0
- package/dist/core/redaction/redactor.js +339 -0
- package/dist/core/redaction/secret-patterns.js +22 -0
- package/dist/core/redaction/value-shapes.js +81 -0
- package/dist/core/redaction/value-trace.js +110 -0
- package/dist/core/review.js +1 -0
- package/dist/core/session-catalogue.js +19 -0
- package/dist/core/session-filter.js +42 -0
- package/dist/core/session-format.js +27 -0
- package/dist/core/session-model.js +1 -0
- package/dist/core/session-reader.js +22 -0
- package/dist/core/session-records.js +1 -0
- package/dist/core/session-source.js +1 -0
- package/dist/core/session-titles.js +21 -0
- package/dist/core/turn.js +1 -0
- package/dist/doctor/codex-doctor.js +14 -0
- package/dist/doctor/doctor-use-case.js +1 -0
- package/dist/doctor/format-selecting-doctor.js +28 -0
- package/dist/doctor/render/attention/attention-rule.js +1 -0
- package/dist/doctor/render/attention/doctor-attention-rules.js +28 -0
- package/dist/doctor/render/attention/rules/damaged-meta-files-rule.js +8 -0
- package/dist/doctor/render/attention/rules/damaged-transcripts-rule.js +9 -0
- package/dist/doctor/render/attention/rules/incomplete-subagent-pairs-rule.js +6 -0
- package/dist/doctor/render/attention/rules/missing-main-transcript-rule.js +5 -0
- package/dist/doctor/render/attention/rules/missing-spilled-results-rule.js +6 -0
- package/dist/doctor/render/attention/rules/multiple-working-directories-rule.js +15 -0
- package/dist/doctor/render/attention/rules/unknown-denial-kinds-rule.js +5 -0
- package/dist/doctor/render/attention/rules/unknown-line-types-rule.js +6 -0
- package/dist/doctor/render/attention/rules/unrecognised-entries-rule.js +6 -0
- package/dist/doctor/render/attention/rules/unusable-directories-rule.js +12 -0
- package/dist/doctor/render/attention/transcript-scopes.js +6 -0
- package/dist/doctor/render/codex-text-doctor-renderer.js +97 -0
- package/dist/doctor/render/doctor-renderers.js +1 -0
- package/dist/doctor/render/json-doctor-renderer.js +7 -0
- package/dist/doctor/render/output-format.js +1 -0
- package/dist/doctor/render/text-doctor-renderer.js +124 -0
- package/dist/doctor/session-doctor.js +16 -0
- package/dist/infrastructure/clack-asker.js +24 -0
- package/dist/infrastructure/clack-chooser.js +40 -0
- package/dist/infrastructure/clack-multi-chooser.js +31 -0
- package/dist/infrastructure/file-alert-store.js +120 -0
- package/dist/infrastructure/file-checked-store.js +55 -0
- package/dist/infrastructure/file-mark-store.js +67 -0
- package/dist/infrastructure/file-onboarding-store.js +93 -0
- package/dist/infrastructure/folder-window.js +26 -0
- package/dist/infrastructure/node-agentwhy-invocation.js +99 -0
- package/dist/infrastructure/node-browser.js +40 -0
- package/dist/infrastructure/node-file-system.js +111 -0
- package/dist/infrastructure/node-local-server.js +115 -0
- package/dist/infrastructure/node-text-input.js +26 -0
- package/dist/infrastructure/os-notifier.js +21 -0
- package/dist/infrastructure/osascript-folder-chooser.js +53 -0
- package/dist/infrastructure/powershell-folder-chooser.js +78 -0
- package/dist/infrastructure/real-directory.js +18 -0
- package/dist/infrastructure/stream-printer.js +11 -0
- package/dist/infrastructure/terminal-banner.js +19 -0
- package/dist/ports/agentwhy-invocation.js +1 -0
- package/dist/ports/alert-store.js +1 -0
- package/dist/ports/asker.js +1 -0
- package/dist/ports/browser.js +1 -0
- package/dist/ports/checked-store.js +1 -0
- package/dist/ports/chooser.js +1 -0
- package/dist/ports/directory-reader.js +1 -0
- package/dist/ports/file-access-error.js +11 -0
- package/dist/ports/file-reader.js +1 -0
- package/dist/ports/file-tail-reader.js +1 -0
- package/dist/ports/file-writer.js +1 -0
- package/dist/ports/folder-chooser.js +1 -0
- package/dist/ports/local-server.js +1 -0
- package/dist/ports/mark-store.js +10 -0
- package/dist/ports/multi-chooser.js +1 -0
- package/dist/ports/notifier.js +1 -0
- package/dist/ports/onboarding-store.js +1 -0
- package/dist/ports/printer.js +1 -0
- package/dist/ports/text-input.js +1 -0
- package/dist/refuse/command-reach.js +135 -0
- package/dist/refuse/command-refusal.js +88 -0
- package/dist/refuse/render/codex-stop-words.js +21 -0
- package/dist/refuse/render/refusal-words.js +34 -0
- package/dist/report/build-report.js +1037 -0
- package/dist/report/check/actions-digest.js +73 -0
- package/dist/report/check/check-lines.js +83 -0
- package/dist/report/check/mark-request.js +90 -0
- package/dist/report/check/marks.js +84 -0
- package/dist/report/check/render/text-digest-renderer.js +332 -0
- package/dist/report/check/session-actions.js +90 -0
- package/dist/report/check/session-check.js +91 -0
- package/dist/report/choose-policy.js +61 -0
- package/dist/report/flow-reads.js +18 -0
- package/dist/report/private-files/tell-lists.js +84 -0
- package/dist/report/project-rules.js +33 -0
- package/dist/report/render/home-relative.js +18 -0
- package/dist/report/render/html-head.js +26 -0
- package/dist/report/render/html-report-components.js +48 -0
- package/dist/report/render/html-report-styles.js +500 -0
- package/dist/report/render/html-sidebar.js +42 -0
- package/dist/report/render/path-tail.js +14 -0
- package/dist/report/render/report-copy.js +1177 -0
- package/dist/report/render/report-page/advanced-view.js +167 -0
- package/dist/report/render/report-page/file-story.js +114 -0
- package/dist/report/render/report-page/files-view.js +381 -0
- package/dist/report/render/report-page/files.js +77 -0
- package/dist/report/render/report-page/fix-wizard-script.js +269 -0
- package/dist/report/render/report-page/fix-wizard.js +310 -0
- package/dist/report/render/report-page/helpers-view.js +383 -0
- package/dist/report/render/report-page/helpers.js +51 -0
- package/dist/report/render/report-page/item-names.js +63 -0
- package/dist/report/render/report-page/protect-patterns.js +13 -0
- package/dist/report/render/report-page/providers.js +208 -0
- package/dist/report/render/report-page/record-view.js +367 -0
- package/dist/report/render/report-page/report-page-renderer.js +129 -0
- package/dist/report/render/report-page/report-views.js +31 -0
- package/dist/report/render/report-page/story-window.js +356 -0
- package/dist/report/render/report-page/times.js +25 -0
- package/dist/report/render/report-page/to-do-view.js +299 -0
- package/dist/report/render/report-page/to-do.js +152 -0
- package/dist/report/render/report-page.js +1 -0
- package/dist/report/render/session-view.js +250 -0
- package/dist/report/render/text-report-renderer.js +981 -0
- package/dist/report/render/ui/add-file-popup.js +122 -0
- package/dist/report/render/ui/app-sidebar.js +78 -0
- package/dist/report/render/ui/ask-panel.js +80 -0
- package/dist/report/render/ui/avatar.js +19 -0
- package/dist/report/render/ui/backdrop.js +29 -0
- package/dist/report/render/ui/brand-mark.js +4 -0
- package/dist/report/render/ui/button.js +67 -0
- package/dist/report/render/ui/callout.js +20 -0
- package/dist/report/render/ui/chat-example.js +17 -0
- package/dist/report/render/ui/checklist.js +50 -0
- package/dist/report/render/ui/confirm-dialog.js +34 -0
- package/dist/report/render/ui/data-table.js +49 -0
- package/dist/report/render/ui/drawer.js +25 -0
- package/dist/report/render/ui/file-chip.js +37 -0
- package/dist/report/render/ui/fold-line.js +22 -0
- package/dist/report/render/ui/guide-card.js +25 -0
- package/dist/report/render/ui/hero.js +37 -0
- package/dist/report/render/ui/labelled-select.js +10 -0
- package/dist/report/render/ui/live-script.js +222 -0
- package/dist/report/render/ui/local-date.js +58 -0
- package/dist/report/render/ui/mode-icon.js +6 -0
- package/dist/report/render/ui/page-shell.js +101 -0
- package/dist/report/render/ui/pill-tabs.js +47 -0
- package/dist/report/render/ui/popup.js +77 -0
- package/dist/report/render/ui/progress.js +35 -0
- package/dist/report/render/ui/project-list.js +284 -0
- package/dist/report/render/ui/scope-choice.js +19 -0
- package/dist/report/render/ui/stats.js +21 -0
- package/dist/report/render/ui/status-icon.js +30 -0
- package/dist/report/render/ui/status-look.js +21 -0
- package/dist/report/render/ui/switch.js +11 -0
- package/dist/report/render/ui/tag.js +19 -0
- package/dist/report/render/ui/task-list.js +56 -0
- package/dist/report/render/ui/tokens.js +46 -0
- package/dist/report/render/ui/update-notice.js +146 -0
- package/dist/report/render/ui/words/app-words.js +163 -0
- package/dist/report/render/ui/words/conversations-words.js +457 -0
- package/dist/report/render/ui/words/month-words.js +59 -0
- package/dist/report/render/ui/words/onboarding-words.js +446 -0
- package/dist/report/render/ui/words/projects-words.js +217 -0
- package/dist/report/render/ui/words/report-words.js +2099 -0
- package/dist/report/render/ui/words/settings-words.js +611 -0
- package/dist/report/render/ui/words/to-fix-words.js +287 -0
- package/dist/report/report-model.js +2 -0
- package/dist/report/report-use-case.js +1 -0
- package/dist/report/rule-names.js +20 -0
- package/dist/report/session-report.js +163 -0
- package/dist/report/start/app-nav.js +37 -0
- package/dist/report/start/conversations/calendar-view.js +104 -0
- package/dist/report/start/conversations/conversation-columns.js +187 -0
- package/dist/report/start/conversations/conversations-renderer.js +54 -0
- package/dist/report/start/conversations/empty-week.js +120 -0
- package/dist/report/start/conversations/files-window.js +166 -0
- package/dist/report/start/conversations/period-section.js +280 -0
- package/dist/report/start/conversations/periods-script.js +440 -0
- package/dist/report/start/conversations/periods.js +110 -0
- package/dist/report/start/conversations/week-view.js +30 -0
- package/dist/report/start/conversations/weeks.js +17 -0
- package/dist/report/start/month/month-renderer.js +50 -0
- package/dist/report/start/month/month-view.js +135 -0
- package/dist/report/start/month/months.js +17 -0
- package/dist/report/start/onboarding/done.js +171 -0
- package/dist/report/start/onboarding/finish-onboarding.js +93 -0
- package/dist/report/start/onboarding/intro.js +100 -0
- package/dist/report/start/onboarding/onboarding-changes.js +40 -0
- package/dist/report/start/onboarding/onboarding-renderer.js +195 -0
- package/dist/report/start/onboarding/onboarding-script.js +400 -0
- package/dist/report/start/onboarding/onboarding-view.js +98 -0
- package/dist/report/start/onboarding/project-step.js +210 -0
- package/dist/report/start/onboarding/steps.js +239 -0
- package/dist/report/start/onboarding/welcome.js +66 -0
- package/dist/report/start/page-notice.js +10 -0
- package/dist/report/start/projects/index-projects.js +43 -0
- package/dist/report/start/projects/moved-page.js +82 -0
- package/dist/report/start/projects/project-switch.js +138 -0
- package/dist/report/start/projects/projects-window.js +67 -0
- package/dist/report/start/projects/temporary-space.js +18 -0
- package/dist/report/start/render/air-datepicker-vendor.js +27 -0
- package/dist/report/start/render/session-status.js +41 -0
- package/dist/report/start/render/start-words.js +84 -0
- package/dist/report/start/repository.js +28 -0
- package/dist/report/start/serve/index-handler.js +372 -0
- package/dist/report/start/serve/onboarding-request.js +50 -0
- package/dist/report/start/serve/settings-request.js +1 -0
- package/dist/report/start/serve/settings-setup.js +193 -0
- package/dist/report/start/session-index.js +1 -0
- package/dist/report/start/session-start.js +974 -0
- package/dist/report/start/settings/alerts-tab.js +104 -0
- package/dist/report/start/settings/files-tab.js +169 -0
- package/dist/report/start/settings/general-tab.js +71 -0
- package/dist/report/start/settings/settings-renderer.js +254 -0
- package/dist/report/start/settings/settings-script.js +161 -0
- package/dist/report/start/settings/settings-view.js +240 -0
- package/dist/report/start/settings/settings-windows.js +334 -0
- package/dist/report/start/settings-files.js +43 -0
- package/dist/report/start/to-fix/done-list.js +45 -0
- package/dist/report/start/to-fix/file-window.js +110 -0
- package/dist/report/start/to-fix/to-fix-list.js +70 -0
- package/dist/report/start/to-fix/to-fix-renderer.js +237 -0
- package/dist/report/start/to-fix/to-fix-script.js +94 -0
- package/dist/report/start/to-fix/to-fix-view.js +124 -0
- package/dist/report/watch/agent-alert.js +72 -0
- package/dist/report/watch/notice-choices.js +35 -0
- package/dist/report/watch/notice-settings.js +65 -0
- package/dist/report/watch/preferences.js +100 -0
- package/dist/report/watch/render/instruction-words.js +54 -0
- package/dist/report/watch/render/notice-words.js +182 -0
- package/dist/report/watch/subagent-watch.js +345 -0
- package/dist/report/watch/watch-notice.js +1 -0
- package/dist/setup/behind.js +18 -0
- package/dist/setup/codex-mirror.js +171 -0
- package/dist/setup/not-a-project.js +19 -0
- package/dist/setup/project-setup.js +644 -0
- package/dist/setup/protected-patterns.js +30 -0
- package/dist/shared/colour.js +23 -0
- package/dist/shared/compare.js +4 -0
- package/dist/shared/content-version.js +13 -0
- package/dist/shared/counter.js +11 -0
- package/dist/shared/file-stamp.js +4 -0
- package/dist/shared/json.js +13 -0
- package/dist/shared/label.js +13 -0
- package/dist/shared/package-name.js +6 -0
- package/dist/shared/plain-invocation.js +44 -0
- package/dist/shared/plain-version.js +21 -0
- package/dist/shared/printable.js +25 -0
- package/dist/shared/renderer.js +1 -0
- package/dist/shared/sentence.js +20 -0
- package/dist/shared/terminal-logo.js +41 -0
- package/dist/shared/text-table.js +15 -0
- package/dist/shared/wrap.js +29 -0
- package/package.json +70 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { matchesGlob } from '../policy/glob.js';
|
|
2
|
+
import { protectionOf } from '../policy/policy.js';
|
|
3
|
+
import { commandPathCandidates, printsContentOnly } from './command-line.js';
|
|
4
|
+
import { listingPathCandidates } from './listing.js';
|
|
5
|
+
import { pathTokens, stringsIn } from './path-tokens.js';
|
|
6
|
+
import { outputIsClean, outputShowsReach } from './recorded-effect.js';
|
|
7
|
+
import { hitLines } from './search-output.js';
|
|
8
|
+
import { printedSearch } from './search-reach.js';
|
|
9
|
+
/**
|
|
10
|
+
* Every touch of a protected path in a session, from both sides.
|
|
11
|
+
*
|
|
12
|
+
* Delegating calls are left out: handing work to another agent reaches no file, and reading a **prompt** for
|
|
13
|
+
* intent is the detector's job, which this milestone does not do. Their effects appear here through the events
|
|
14
|
+
* of the agent they started.
|
|
15
|
+
*
|
|
16
|
+
* Outcome is carried, not judged. §5.4 attaches `SUCCEEDED` to its definition of `S-input`, but that belongs to
|
|
17
|
+
* a verdict, and a refused attempt is worth reading too - it says the policy held.
|
|
18
|
+
*/
|
|
19
|
+
export function protectedAccesses(model, policy) {
|
|
20
|
+
const delegating = new Set(model.delegations.map((delegation) => delegation.id));
|
|
21
|
+
return model.events
|
|
22
|
+
.filter((event) => !delegating.has(event.id))
|
|
23
|
+
.flatMap((event) => accessesOf(event, policy));
|
|
24
|
+
}
|
|
25
|
+
function accessesOf(event, policy) {
|
|
26
|
+
// A path parameter is a path. A command line is read for its arguments, and a listing for what it enumerates:
|
|
27
|
+
// both have structure, and using it is what separates a file that was reached from a file that was mentioned.
|
|
28
|
+
// A path parameter and a quoted shell argument are both positional: something said "this is one thing", so a
|
|
29
|
+
// space inside belongs to it. Only guessed candidates - a line of output, a word taken out of prose - have to
|
|
30
|
+
// be whitespace-free.
|
|
31
|
+
const fromInput = dedupe([
|
|
32
|
+
...protectedTargets(event.targets, policy),
|
|
33
|
+
...protectedPathsAmong(event.commands.flatMap(commandPathCandidates), policy, { allowWhitespace: true }),
|
|
34
|
+
]);
|
|
35
|
+
const named = new Set(fromInput.map(([path]) => path));
|
|
36
|
+
// Only a listing says the call reached what it names. The content of a file that mentions `.env` says the
|
|
37
|
+
// file mentions it - counting that was worth 316 findings on a session that had a handful.
|
|
38
|
+
// A tool's declared shape is refined by what was actually run: `cat` through a shell prints a file, and the
|
|
39
|
+
// paths in that file are its text. The shape belongs to the command, one level below the tool.
|
|
40
|
+
const enumerates = event.resultShape === 'listing' && !readsContent(event);
|
|
41
|
+
// X11: a process that did not complete cleanly may have printed diagnostics naming what it could not open, so of its
|
|
42
|
+
// output only a field in a search hit's position names a file it reached.
|
|
43
|
+
const clean = outputIsClean(event.execution);
|
|
44
|
+
const listed = enumerates
|
|
45
|
+
? stringsIn(event.result?.content).flatMap(listingPathCandidates).map((line) => (clean ? line : line.filter((candidate) => candidate.positional)))
|
|
46
|
+
: [];
|
|
47
|
+
const shown = protectedPathsPerLine(listed, policy);
|
|
48
|
+
// A path the parameters already named is not an `S-result`: the source is what the other tools would miss.
|
|
49
|
+
const fromResult = shown.filter(([path]) => !named.has(path));
|
|
50
|
+
const printed = printedLines(event, [...named]);
|
|
51
|
+
const outcomeOf = targetOutcome(event, printed, new Set(shown.map(([path]) => path)));
|
|
52
|
+
// H1: lines a search printed are text the agent read only where the result is what its model was handed (X10).
|
|
53
|
+
const lines = event.result?.stage === 'model' ? new Map([...printed].map(([path, each]) => [path, each.length])) : new Map();
|
|
54
|
+
return [
|
|
55
|
+
...fromInput.map(([path, pattern]) => toAccess(event, 'input', path, pattern, outcomeOf(path, 'input'), lines.get(path))),
|
|
56
|
+
...fromResult.map(([path, pattern]) => toAccess(event, 'result', path, pattern, outcomeOf(path, 'result'), lines.get(path))),
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The outcome of one target of a call (X11). The call's own outcome where the record establishes it - or where the format
|
|
61
|
+
* keeps no process record, as Claude Code's does not. Where a process ran and its effect is not established, what its
|
|
62
|
+
* output shows it reached it reached: a line naming the file, printed by a search or a listing. Any other target is an
|
|
63
|
+
* attempt whose effect is unknown - never counted as reached because the command ran.
|
|
64
|
+
*/
|
|
65
|
+
function targetOutcome(event, printed, shown) {
|
|
66
|
+
return (path, source) => {
|
|
67
|
+
if (event.outcome !== 'unknown' || event.execution === undefined || event.result?.content === undefined)
|
|
68
|
+
return event.outcome;
|
|
69
|
+
return source === 'result' || printed.has(path) || shown.has(path) ? 'succeeded' : 'unknown';
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** A search hit with its line number: `path:12:text`. A diagnostic `path: message` has none. */
|
|
73
|
+
const NUMBERED_HIT = /^[^:]+:\d+:/;
|
|
74
|
+
/** How many lines of each file a succeeded search printed (H1, H2, H12): `printedLines`, counted. */
|
|
75
|
+
export function linesPrinted(event, named) {
|
|
76
|
+
return new Map([...printedLines(event, named)].map(([path, lines]) => [path, lines.length]));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The lines of each file a succeeded search printed, per file (H1, H2, H12). Where the search puts no file's name on its
|
|
80
|
+
* lines and the call named exactly one protected file - `grep KEY .env` - its whole output is that file's lines (H3), as
|
|
81
|
+
* a `cat` of it is. Whether it puts one there is the command's to say (`PrintedSearch.names`), not the output's: a
|
|
82
|
+
* colon in a line of `.env` is not a file name before it. **Raw content**: for the redactor, and nothing else (H11).
|
|
83
|
+
*/
|
|
84
|
+
export function printedLines(event, named) {
|
|
85
|
+
const printed = new Map();
|
|
86
|
+
const search = searchOf(event);
|
|
87
|
+
if (search === undefined || !outputShowsReach(event))
|
|
88
|
+
return printed;
|
|
89
|
+
// X11: a process that did not complete cleanly may have printed `.env: Permission denied`, which reads as a hit. Only a
|
|
90
|
+
// numbered hit line is taken from it, and its whole output is never one named file's (H3).
|
|
91
|
+
if (!outputIsClean(event.execution)) {
|
|
92
|
+
const numbered = stringsIn(event.result?.content).join('\n').split('\n').filter((line) => NUMBERED_HIT.test(line)).join('\n');
|
|
93
|
+
for (const hit of hitLines(numbered, false))
|
|
94
|
+
printed.set(hit.path, [...(printed.get(hit.path) ?? []), hit.text]);
|
|
95
|
+
return printed;
|
|
96
|
+
}
|
|
97
|
+
const output = stringsIn(event.result?.content).join('\n');
|
|
98
|
+
const only = [...new Set(named)];
|
|
99
|
+
const whole = () => output.split('\n')
|
|
100
|
+
.filter((line) => line.trim() !== '' && line !== '--')
|
|
101
|
+
.map((line) => (search.numbered === true ? line.replace(/^\d+[:-]/, '') : line));
|
|
102
|
+
// H3: lines with no name on them are the one file the call named - or no one's that can be told.
|
|
103
|
+
if (search.names === 'never') {
|
|
104
|
+
if (only.length === 1 && whole().length > 0)
|
|
105
|
+
printed.set(only[0], whole());
|
|
106
|
+
return printed;
|
|
107
|
+
}
|
|
108
|
+
const hits = hitLines(output, search.context);
|
|
109
|
+
for (const hit of hits)
|
|
110
|
+
printed.set(hit.path, [...(printed.get(hit.path) ?? []), hit.text]);
|
|
111
|
+
// One operand that may be a file: where it is the one protected file named and no line names it, no line names any.
|
|
112
|
+
if (search.names === 'maybe' && only.length === 1 && !printed.has(only[0])) {
|
|
113
|
+
printed.clear();
|
|
114
|
+
if (whole().length > 0)
|
|
115
|
+
printed.set(only[0], whole());
|
|
116
|
+
}
|
|
117
|
+
return printed;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The call is a search that prints what it matched: a shell search read from its command, or a tool that said so. The
|
|
121
|
+
* Grep tool may or may not name the file on its lines, so that is left to what it printed.
|
|
122
|
+
*/
|
|
123
|
+
export function searchOf(event) {
|
|
124
|
+
if (event.printsMatches === true)
|
|
125
|
+
return { context: true, names: 'maybe' };
|
|
126
|
+
if (event.commands.length === 0)
|
|
127
|
+
return undefined;
|
|
128
|
+
return event.commands.map(printedSearch).find((search) => search !== undefined);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Whether what came back is the text of what the call named - a `Read`, or a shell line that only prints (`cat`) -
|
|
132
|
+
* rather than a list of what it found. A tool's declared shape is refined by what was actually run, as `accessesOf`
|
|
133
|
+
* reads it: this is the one place that decision is made.
|
|
134
|
+
*/
|
|
135
|
+
export function readsContent(event) {
|
|
136
|
+
return event.resultShape === 'content' || (event.resultShape === 'listing' && printsContentOnly(event.commands));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The protected paths one command line names, with the pattern that protects each - read exactly as a report reads the
|
|
140
|
+
* command of a call, so a hook refusing a command and the report on the session cannot disagree about whether it named
|
|
141
|
+
* a protected path (`specs/2026-09-16-worth-running-every-day.md` R18). Only what the line names: what the command would
|
|
142
|
+
* print is not known before it runs.
|
|
143
|
+
*/
|
|
144
|
+
export function protectedPathsInCommand(command, policy) {
|
|
145
|
+
return protectedPathsAmong(commandPathCandidates(command), policy, { allowWhitespace: true });
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A candidate holding whitespace is not offered to the policy. A path may legitimately contain a space, and
|
|
149
|
+
* that is the cost; what is bought is every banner, diff header and sentence that happens to end in a path -
|
|
150
|
+
* `--- apps/web/.npmrc` was reported as a file, and prose that trails off into a path was reported as one too.
|
|
151
|
+
* Measured on one session: it is the difference between 85 findings and 76.
|
|
152
|
+
*/
|
|
153
|
+
function hasNoWhitespace(candidate) {
|
|
154
|
+
return !/\s/.test(candidate);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A backslash in a candidate is an escape, and an escape belongs to a regular expression or to shell quoting -
|
|
158
|
+
* `grep '\.env'` was being reported as a file called `\.env`. A path may legally hold one, and almost never
|
|
159
|
+
* does on the systems this reads.
|
|
160
|
+
*/
|
|
161
|
+
function isNotEscaped(candidate) {
|
|
162
|
+
return !candidate.includes('\\');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A name with no separator in it is a path only if it is a dotfile. Without this, the `env` property that every
|
|
166
|
+
* TypeScript repository reads off Node's global process object is reported as a file called `.env`, and so is
|
|
167
|
+
* `import.meta.env`. Dotted code and a dotted filename look alike; where the dot sits is what tells them apart.
|
|
168
|
+
*
|
|
169
|
+
* The cost is a file called `config.env` sitting in the working directory and named without a path. That is
|
|
170
|
+
* rarer than the code, and it is a rule rather than a list of identifiers to keep extending.
|
|
171
|
+
*
|
|
172
|
+
* *Narrowed 2026-09-24* (`protectionFor`): it holds for a guessed candidate only. A person who wrote a rule naming
|
|
173
|
+
* `customers.csv` had an agent run `cat customers.csv` and `grep -r … .`, whose hit came back as `customers.csv:6:…`
|
|
174
|
+
* with a customer's name, e-mail and phone in it - and the report said "nothing private" of both.
|
|
175
|
+
*/
|
|
176
|
+
function looksAddressable(candidate) {
|
|
177
|
+
return candidate.includes('/') || candidate.startsWith('.');
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The entry that protects a candidate, where a candidate of its shape may be a file at all. A bare name - no separator,
|
|
181
|
+
* no leading dot - is one where either:
|
|
182
|
+
*
|
|
183
|
+
* - **its position says so**: the value a tool was handed, or the field before a search hit's first colon, is a file
|
|
184
|
+
* name because of where it sits, whatever it looks like; or
|
|
185
|
+
* - **a rule names it exactly**: a pattern whose last part holds no wildcard - one ending in `customers.csv` - was
|
|
186
|
+
* written for that one file. The `env` read off Node's process object is matched only by a wildcard (`*.env`),
|
|
187
|
+
* which is what the rule above is for.
|
|
188
|
+
*/
|
|
189
|
+
function protectionFor(policy, candidate, positional) {
|
|
190
|
+
const protection = protectionOf(policy, candidate);
|
|
191
|
+
if (protection === undefined || positional || looksAddressable(candidate))
|
|
192
|
+
return protection;
|
|
193
|
+
return policy.protected.find((entry) => namesOneFile(entry.pattern) && matchesGlob(candidate, entry.pattern));
|
|
194
|
+
}
|
|
195
|
+
/** A pattern whose last part is a name, not a wildcard: it was written for one file. */
|
|
196
|
+
function namesOneFile(pattern) {
|
|
197
|
+
const last = pattern.slice(pattern.lastIndexOf('/') + 1);
|
|
198
|
+
return last !== '' && !GLOB.test(last);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* A bracket or a quote left in a candidate means it is an expression, not a file. A command line has its paths
|
|
202
|
+
* lifted out of such expressions before they get here (`command-line.ts`); a line of output does not, because
|
|
203
|
+
* the text of a file is not something this tool is entitled to parse.
|
|
204
|
+
*/
|
|
205
|
+
const CODE_PUNCTUATION = /[()'"`]/;
|
|
206
|
+
const GLOB = /[*?]/;
|
|
207
|
+
/**
|
|
208
|
+
* A search hit is not a path. `grep -n` prints `path:12:KEY=value`, and that whole string was being reported as the
|
|
209
|
+
* name of a file - in a report, and then as a line of `check`, where it also carried a redacted value into a column of
|
|
210
|
+
* paths. The path is the field before the first colon, and `listing.ts` already lifts it out for a result; this rejects
|
|
211
|
+
* what is left when such a line arrives from somewhere else. Measured on real sessions.
|
|
212
|
+
*/
|
|
213
|
+
const SEARCH_HIT = /:\d+:/;
|
|
214
|
+
/**
|
|
215
|
+
* An assignment is not a path either: `KEY=value` is what a file holds, not a file. A path may legally contain `=`,
|
|
216
|
+
* and practically never does.
|
|
217
|
+
*/
|
|
218
|
+
const ASSIGNMENT = /=/;
|
|
219
|
+
/**
|
|
220
|
+
* Everything that has to be true before a string is offered to the policy as a path someone reached. Each part
|
|
221
|
+
* was written for a false positive that was actually measured on a real session, and is named for it.
|
|
222
|
+
*/
|
|
223
|
+
function pathLike(candidate, options) {
|
|
224
|
+
return (candidate !== '' &&
|
|
225
|
+
isNotEscaped(candidate) &&
|
|
226
|
+
!CODE_PUNCTUATION.test(candidate) &&
|
|
227
|
+
// A leading dash is an option or a diff marker: `--- apps/web/.npmrc` is a banner above a file, not a file.
|
|
228
|
+
!candidate.startsWith('-') &&
|
|
229
|
+
// A glob is a question, not an answer. `ls .env*` names no file, and reporting `.env*` as one reached puts
|
|
230
|
+
// a string in the report that exists nowhere on disk. What such a command actually opened, if anything,
|
|
231
|
+
// comes back in its output and is found there instead.
|
|
232
|
+
!GLOB.test(candidate) &&
|
|
233
|
+
!SEARCH_HIT.test(candidate) &&
|
|
234
|
+
!ASSIGNMENT.test(candidate) &&
|
|
235
|
+
(options.allowWhitespace || hasNoWhitespace(candidate)));
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* A target is not text that happens to contain a path - it is the value the tool was handed, and the tool's
|
|
239
|
+
* profile says that value names the file. So the whole value is offered first, and only if the policy does not
|
|
240
|
+
* protect it are its tokens tried.
|
|
241
|
+
*
|
|
242
|
+
* Splitting first was wrong in a way that showed up in a report: a project at `/Users/someone/Client Name/app` had
|
|
243
|
+
* every finding under it cut at the space, so `Client Name/app/apps/web/.env` was reported as a file called
|
|
244
|
+
* `Name/app/apps/web/.env` - a path that exists nowhere. One target names one file, so the first match wins.
|
|
245
|
+
*/
|
|
246
|
+
function protectedTargets(targets, policy) {
|
|
247
|
+
const found = new Map();
|
|
248
|
+
for (const target of targets) {
|
|
249
|
+
for (const candidate of [target, ...pathTokens(target)]) {
|
|
250
|
+
// Whitespace disqualifies a token guessed out of text, never the value the tool was given.
|
|
251
|
+
if (!pathLike(candidate, { allowWhitespace: candidate === target }))
|
|
252
|
+
continue;
|
|
253
|
+
const protection = protectionFor(policy, candidate, candidate === target);
|
|
254
|
+
if (protection === undefined)
|
|
255
|
+
continue;
|
|
256
|
+
if (!found.has(candidate))
|
|
257
|
+
found.set(candidate, protection.pattern);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return [...found];
|
|
262
|
+
}
|
|
263
|
+
/** Each path once, keeping the pattern it was first matched by. */
|
|
264
|
+
function dedupe(entries) {
|
|
265
|
+
return [...new Map(entries.map(([path, pattern]) => [path, pattern]))];
|
|
266
|
+
}
|
|
267
|
+
/** Pairs of the path and the pattern that protects it, each path once, in the order they were met. */
|
|
268
|
+
function protectedPathsAmong(candidates, policy, options = {}) {
|
|
269
|
+
const found = new Map();
|
|
270
|
+
const usable = candidates.filter((entry) => pathLike(entry, { allowWhitespace: options.allowWhitespace === true }));
|
|
271
|
+
for (const candidate of usable) {
|
|
272
|
+
if (found.has(candidate))
|
|
273
|
+
continue;
|
|
274
|
+
const protection = protectionFor(policy, candidate, false);
|
|
275
|
+
if (protection !== undefined)
|
|
276
|
+
found.set(candidate, protection.pattern);
|
|
277
|
+
}
|
|
278
|
+
return [...found];
|
|
279
|
+
}
|
|
280
|
+
/** One line of a listing names one file: the first candidate that the policy protects, and no more. */
|
|
281
|
+
function protectedPathsPerLine(lines, policy) {
|
|
282
|
+
const found = new Map();
|
|
283
|
+
for (const candidates of lines) {
|
|
284
|
+
// Whitespace disqualifies a guessed candidate but not a positional one: the field before a search hit's
|
|
285
|
+
// first colon is a path because of where it sits, space in a directory name and all.
|
|
286
|
+
const usable = candidates.filter((entry) => pathLike(entry.text, { allowWhitespace: entry.positional }));
|
|
287
|
+
for (const { text: candidate, positional } of usable) {
|
|
288
|
+
const protection = protectionFor(policy, candidate, positional);
|
|
289
|
+
if (protection === undefined)
|
|
290
|
+
continue;
|
|
291
|
+
if (!found.has(candidate))
|
|
292
|
+
found.set(candidate, protection.pattern);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return [...found];
|
|
297
|
+
}
|
|
298
|
+
function toAccess(event, source, path, pattern, outcome, lines) {
|
|
299
|
+
return {
|
|
300
|
+
...(lines === undefined ? {} : { lines }),
|
|
301
|
+
eventId: event.id,
|
|
302
|
+
agentId: event.agentId,
|
|
303
|
+
toolName: event.toolName,
|
|
304
|
+
source,
|
|
305
|
+
path,
|
|
306
|
+
pattern,
|
|
307
|
+
outcome,
|
|
308
|
+
evidence: source === 'input' ? event.evidence : (event.result?.evidence ?? event.evidence),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { unquote } from '../redaction/value-shapes.js';
|
|
2
|
+
import { codeReadsNamedFile, printsContentOnly } from './command-line.js';
|
|
3
|
+
const KEYED = /^(?:export\s+)?[A-Za-z_][A-Za-z0-9_.-]*\s*[=:]\s*(.*)$/;
|
|
4
|
+
const JSON_KEYED = /^"[^"]+"\s*:\s*("[^"]*"|[^,\s]+)/;
|
|
5
|
+
/**
|
|
6
|
+
* An `.npmrc` setting scoped to a registry, as npm documents it: `//registry.npmjs.org/:_authToken=value`. Found by
|
|
7
|
+
* `docs/detection.md` (`npmrc`): read as a line of its own, the whole setting looked like an address and was dropped as
|
|
8
|
+
* an ordinary value, so a token `cat ~/.npmrc` printed was never traced.
|
|
9
|
+
*/
|
|
10
|
+
const REGISTRY_KEYED = /^\/\/\S+?:[A-Za-z_][A-Za-z0-9_-]*\s*=\s*(.*)$/;
|
|
11
|
+
/** A `Read` numbers the lines it returns. */
|
|
12
|
+
const LINE_NUMBER = /^\s*\d+→/;
|
|
13
|
+
/**
|
|
14
|
+
* Every value read out of a protected resource by a call that succeeded, each once per agent and file:
|
|
15
|
+
*
|
|
16
|
+
* - from the content a call returned when it named the resource and printed what was in it - a `Read`, a `cat`;
|
|
17
|
+
* - from what an interpreter printed when the code it was handed names the resource - `python3 -c "print(open('.env').read())"`;
|
|
18
|
+
* - from a listing line that names a protected path - `path:line:KEY=value`, the motivating case.
|
|
19
|
+
*
|
|
20
|
+
* A line shaped `KEY=value` or `"key": "value"` gives its value; any other line gives itself.
|
|
21
|
+
*
|
|
22
|
+
* A call's content is read **once for the call**, against every protected path it named. `cat .env .env.local`
|
|
23
|
+
* concatenates two files with nothing between them, and a line of that output came from one of them; recording
|
|
24
|
+
* it once against each, as if each file held it, puts a file in the report that the value was never read from.
|
|
25
|
+
* A listing line carries its own path, so those are attributed one by one and stay exact.
|
|
26
|
+
*
|
|
27
|
+
* **Known limit.** A search that names one file prints its matches without the file's name - `grep KEY .env`
|
|
28
|
+
* answers `12:KEY=value` - and those lines are neither content nor a listing line naming a path, so their values
|
|
29
|
+
* are not read. Recorded rather than guessed at, until a measurement says how often it happens.
|
|
30
|
+
*/
|
|
31
|
+
export function protectedValues(model, accesses) {
|
|
32
|
+
const events = new Map(model.events.map((event) => [event.id, event]));
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
const values = [];
|
|
35
|
+
const add = (agentId, paths, line) => {
|
|
36
|
+
const value = valueOf(line);
|
|
37
|
+
const key = JSON.stringify([agentId, paths, value]);
|
|
38
|
+
if (value === undefined || seen.has(key))
|
|
39
|
+
return;
|
|
40
|
+
seen.add(key);
|
|
41
|
+
values.push({ agentId, paths, value });
|
|
42
|
+
};
|
|
43
|
+
for (const [eventId, group] of byEvent(accesses)) {
|
|
44
|
+
const event = events.get(eventId);
|
|
45
|
+
const content = event?.result?.content;
|
|
46
|
+
if (event === undefined || content === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
const named = [...new Set(group.filter((access) => access.source === 'input').map((access) => access.path))];
|
|
49
|
+
if (named.length > 0 && (event.resultShape === 'content' || printsContentOnly(event.commands))) {
|
|
50
|
+
for (const line of content.split('\n'))
|
|
51
|
+
add(event.agentId, named, line);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// An interpreter whose own code opened the file: its output is read as the file's, against the files it named.
|
|
55
|
+
const opened = named.filter((path) => codeReadsNamedFile(event.commands, path));
|
|
56
|
+
if (opened.length > 0)
|
|
57
|
+
for (const line of content.split('\n'))
|
|
58
|
+
add(event.agentId, opened, line);
|
|
59
|
+
}
|
|
60
|
+
for (const access of group) {
|
|
61
|
+
if (access.source !== 'result')
|
|
62
|
+
continue;
|
|
63
|
+
for (const line of listedLines(content, access.path))
|
|
64
|
+
add(access.agentId, [access.path], line);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return values;
|
|
68
|
+
}
|
|
69
|
+
/** The succeeded accesses of each call, in the order the calls were met: one call's content is read once. */
|
|
70
|
+
function byEvent(accesses) {
|
|
71
|
+
const groups = new Map();
|
|
72
|
+
for (const access of accesses) {
|
|
73
|
+
if (access.outcome !== 'succeeded')
|
|
74
|
+
continue;
|
|
75
|
+
const group = groups.get(access.eventId);
|
|
76
|
+
if (group === undefined)
|
|
77
|
+
groups.set(access.eventId, [access]);
|
|
78
|
+
else
|
|
79
|
+
group.push(access);
|
|
80
|
+
}
|
|
81
|
+
return groups;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The value of a `KEY=value`, `"key": "value"` or `//registry/:key=value` line, or the line itself; nothing for a blank
|
|
85
|
+
* line or a comment.
|
|
86
|
+
*/
|
|
87
|
+
function valueOf(raw) {
|
|
88
|
+
const line = raw.replace(LINE_NUMBER, '').trim();
|
|
89
|
+
if (line === '' || line.startsWith('#'))
|
|
90
|
+
return undefined;
|
|
91
|
+
const json = JSON_KEYED.exec(line);
|
|
92
|
+
const keyed = json === null ? (KEYED.exec(line) ?? REGISTRY_KEYED.exec(line)) : null;
|
|
93
|
+
const value = json !== null ? unquote(json[1] ?? '') : keyed !== null ? unquote((keyed[1] ?? '').trim()) : line;
|
|
94
|
+
return value === '' ? undefined : value;
|
|
95
|
+
}
|
|
96
|
+
/** The lines of a listing that name this path, with the path and a line number taken off the front. */
|
|
97
|
+
function listedLines(content, path) {
|
|
98
|
+
return content
|
|
99
|
+
.split('\n')
|
|
100
|
+
.filter((line) => line.startsWith(`${path}:`))
|
|
101
|
+
.map((line) => line.slice(path.length + 1).replace(/^\d+:/, ''));
|
|
102
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { simpleCommandsIn } from './command-line.js';
|
|
2
|
+
import { readOptions } from './search-reach.js';
|
|
3
|
+
/**
|
|
4
|
+
* Programs that open every file operand they are given, print it, and exit non-zero when one of them cannot be opened.
|
|
5
|
+
* With a recorded exit code of 0, each operand was read. Any other program's exit code establishes nothing about its
|
|
6
|
+
* operands here: `grep` exits 1 for a file it read and found nothing in, and a script exits however it likes.
|
|
7
|
+
*/
|
|
8
|
+
const READS_EVERY_OPERAND = new Set(['cat', 'head', 'tail', 'nl']);
|
|
9
|
+
/**
|
|
10
|
+
* `sed` opens its file operands in turn and exits non-zero on one it cannot open, but a script that quits (`q`) stops
|
|
11
|
+
* before the next: with exit 0, only a lone operand is known to have been read. `-e` and `-f` give the script, so every
|
|
12
|
+
* positional word is then an operand. Any option not listed - `-i`, whose suffix BSD takes from the next word, or `--help`,
|
|
13
|
+
* which opens nothing - leaves the effect unknown.
|
|
14
|
+
*/
|
|
15
|
+
const SED_OPTIONS = { shortWithValue: new Set(['e', 'f']), longWithValue: new Set(['--expression', '--file']) };
|
|
16
|
+
const SED_SCRIPT_OPTIONS = new Set(['-e', '-f', '--expression', '--file']);
|
|
17
|
+
const SED_PLAIN_OPTIONS = new Set([
|
|
18
|
+
'-n', '-E', '-r', '-s', '-u', '-z', '--quiet', '--silent', '--regexp-extended', '--separate', '--unbuffered', '--null-data', '--posix',
|
|
19
|
+
'--sandbox',
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* What a runtime's record of running a call establishes about access (`2026-09-27-what-codex-wrote.md` X9, X11). A
|
|
23
|
+
* status says that something ran, never what it reached:
|
|
24
|
+
*
|
|
25
|
+
* - a recognised call that writes, recorded as completed: its write happened;
|
|
26
|
+
* - a recognised command line of exactly one simple command whose program reads every operand, or a `sed` given one,
|
|
27
|
+
* completed with exit 0: each operand was read;
|
|
28
|
+
* - anything else - a failure, an interruption, another program, a pipeline, a tool with no profile: `unknown`. A
|
|
29
|
+
* requested path alone is an attempt. What the output itself shows is read per target by `protectedAccesses`.
|
|
30
|
+
*/
|
|
31
|
+
export function outcomeOfExecution(call, execution) {
|
|
32
|
+
if (!call.toolKnown || execution.status !== 'completed')
|
|
33
|
+
return 'unknown';
|
|
34
|
+
if (call.written !== undefined)
|
|
35
|
+
return 'succeeded';
|
|
36
|
+
if (call.commands.length !== 1 || execution.exitCode !== 0)
|
|
37
|
+
return 'unknown';
|
|
38
|
+
const simple = simpleCommandsIn(call.commands[0] ?? '');
|
|
39
|
+
return simple.length === 1 && simple[0] !== undefined && readsItsOperands(simple[0]) ? 'succeeded' : 'unknown';
|
|
40
|
+
}
|
|
41
|
+
function readsItsOperands({ program, args }) {
|
|
42
|
+
if (READS_EVERY_OPERAND.has(program))
|
|
43
|
+
return true;
|
|
44
|
+
if (program !== 'sed')
|
|
45
|
+
return false;
|
|
46
|
+
const { options, positionals } = readOptions(args, SED_OPTIONS);
|
|
47
|
+
if (!options.every((option) => SED_SCRIPT_OPTIONS.has(option.name) || SED_PLAIN_OPTIONS.has(option.name)))
|
|
48
|
+
return false;
|
|
49
|
+
const scriptGiven = options.some((option) => SED_SCRIPT_OPTIONS.has(option.name));
|
|
50
|
+
return positionals.length - (scriptGiven ? 0 : 1) === 1;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether a call's recorded output shows what it reached, whatever its event outcome: a result the record establishes, or
|
|
54
|
+
* the output of a process that ran - where a line naming its file (`path:12:…`) shows that file was read even when the
|
|
55
|
+
* process then failed on another (X11).
|
|
56
|
+
*/
|
|
57
|
+
export function outputShowsReach(event) {
|
|
58
|
+
return event.outcome === 'succeeded' || (event.outcome === 'unknown' && event.execution !== undefined);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether every line of a process's output may name what it reached: the process completed with exit 0. Otherwise its
|
|
62
|
+
* output may hold diagnostics that name paths it could not open, so only lines that carry a file name in a search hit's
|
|
63
|
+
* position are read as reached.
|
|
64
|
+
*/
|
|
65
|
+
export function outputIsClean(execution) {
|
|
66
|
+
return execution === undefined || (execution.status === 'completed' && execution.exitCode === 0);
|
|
67
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { isOrdinaryShape } from '../redaction/value-shapes.js';
|
|
2
|
+
import { protectedValues } from './protected-values.js';
|
|
3
|
+
export function traceValues(model, accesses, traceOf) {
|
|
4
|
+
// Spec §5.2: an ordinary value - a port, a boolean - would make every report that mentions one carry a value back.
|
|
5
|
+
const values = protectedValues(model, accesses).filter((value) => !isOrdinaryShape(value.value));
|
|
6
|
+
return { values, trace: traceOf(values.map((value) => value.value)) };
|
|
7
|
+
}
|
|
8
|
+
/** Which protected files a traced value in this text was read from. Empty when it carries none. */
|
|
9
|
+
export function filesTracedIn(texts, traced) {
|
|
10
|
+
const files = new Set();
|
|
11
|
+
for (const text of texts) {
|
|
12
|
+
for (const index of traced.trace.foundIn(text)) {
|
|
13
|
+
const value = traced.values[index];
|
|
14
|
+
if (value !== undefined)
|
|
15
|
+
for (const path of value.paths)
|
|
16
|
+
files.add(path);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [...files].sort();
|
|
20
|
+
}
|
|
21
|
+
export function returnsOf(model, accesses, traced) {
|
|
22
|
+
const { values, trace } = traced;
|
|
23
|
+
const events = new Map(model.events.map((event) => [event.id, event]));
|
|
24
|
+
return model.delegations.flatMap((delegation) => {
|
|
25
|
+
const child = delegation.childAgentId;
|
|
26
|
+
if (child === undefined)
|
|
27
|
+
return [];
|
|
28
|
+
const below = agentsFrom(model, child);
|
|
29
|
+
// Only a confirmed reach could have given the agent something to return: a refusal did not, and an attempt whose
|
|
30
|
+
// effect the record does not establish is no reach (`2026-09-27-what-codex-wrote.md` X11).
|
|
31
|
+
const reached = accesses.filter((access) => below.has(access.agentId) && access.outcome === 'succeeded');
|
|
32
|
+
// Nothing protected was reached at or below it, so nothing protected could have come back.
|
|
33
|
+
if (reached.length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
const event = events.get(delegation.id);
|
|
36
|
+
const result = event?.result;
|
|
37
|
+
const readable = event !== undefined && event.outcome !== 'unknown' && result?.content !== undefined;
|
|
38
|
+
// `2026-09-15-where-the-value-went.md` R3: what came back is what the delegating agent received - the result, and
|
|
39
|
+
// every report delivered for it. A launch notice says only that the agent started, so it is not one of them.
|
|
40
|
+
const received = [
|
|
41
|
+
...(readable && result?.content !== undefined && result.launchNotice !== true
|
|
42
|
+
? [{ text: result.content, evidence: result.evidence }]
|
|
43
|
+
: []),
|
|
44
|
+
...delegation.reports.map((report) => ({ text: report.content, evidence: report.evidence })),
|
|
45
|
+
];
|
|
46
|
+
const messages = model.messages.filter((message) => message.agentId === child).map((message) => message.text);
|
|
47
|
+
const reportLength = delegation.reports.length > 0
|
|
48
|
+
? delegation.reports.reduce((length, report) => length + report.content.length, 0)
|
|
49
|
+
: result?.content?.length;
|
|
50
|
+
const base = {
|
|
51
|
+
delegationId: delegation.id,
|
|
52
|
+
agentId: child,
|
|
53
|
+
parentAgentId: delegation.parentAgentId,
|
|
54
|
+
filesReached: new Set(reached.map((access) => access.path)).size,
|
|
55
|
+
...(reportLength === undefined ? {} : { reportLength }),
|
|
56
|
+
evidence: event?.result?.evidence ?? event?.evidence ?? delegation.evidence,
|
|
57
|
+
};
|
|
58
|
+
// A value read by a call that printed several protected files at once came from one of them, so the files it
|
|
59
|
+
// was found under are carried with whether they are the one or the several.
|
|
60
|
+
const filesOf = (texts) => {
|
|
61
|
+
const files = new Set();
|
|
62
|
+
let uncertain = false;
|
|
63
|
+
for (const text of texts) {
|
|
64
|
+
for (const index of trace.foundIn(text)) {
|
|
65
|
+
const value = values[index];
|
|
66
|
+
if (value === undefined || !below.has(value.agentId))
|
|
67
|
+
continue;
|
|
68
|
+
for (const path of value.paths)
|
|
69
|
+
files.add(path);
|
|
70
|
+
if (value.paths.length > 1)
|
|
71
|
+
uncertain = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { paths: [...files].sort(), uncertain };
|
|
75
|
+
};
|
|
76
|
+
const written = filesOf(messages);
|
|
77
|
+
const writtenFrom = written.paths;
|
|
78
|
+
const shown = (files) => files.uncertain ? { fileUncertain: true } : {};
|
|
79
|
+
// What was received decides what came back (R2, amended). A statement of a value shows where the value came from,
|
|
80
|
+
// says nothing of what the agent wrote (R4b), and carries the record of the first text that held it. It is made
|
|
81
|
+
// before anything unreadable is weighed: a value that came back is never downgraded (R7).
|
|
82
|
+
const fromReceived = filesOf(received.map((entry) => entry.text));
|
|
83
|
+
if (fromReceived.paths.length > 0) {
|
|
84
|
+
const carrying = received.find((entry) => filesOf([entry.text]).paths.length > 0);
|
|
85
|
+
return [{
|
|
86
|
+
...base,
|
|
87
|
+
...(carrying === undefined ? {} : { evidence: carrying.evidence }),
|
|
88
|
+
strength: 'value',
|
|
89
|
+
paths: fromReceived.paths,
|
|
90
|
+
writtenFrom,
|
|
91
|
+
...shown(fromReceived),
|
|
92
|
+
}];
|
|
93
|
+
}
|
|
94
|
+
// R9: a return that cannot be read is not a clean one, whatever the agent wrote.
|
|
95
|
+
if (!readable)
|
|
96
|
+
return [{ ...base, strength: 'unknown', paths: [], writtenFrom, ...shown(written) }];
|
|
97
|
+
// where-the-value-went R5: started in the background, and no report delivered - what came back is not in the record.
|
|
98
|
+
if (received.length === 0)
|
|
99
|
+
return [{ ...base, strength: 'unknown', awaited: true, paths: [], writtenFrom, ...shown(written) }];
|
|
100
|
+
// R5: only a file that was reached below counts. A protected path a report merely mentions is prose.
|
|
101
|
+
const named = [...new Set(reached.map((access) => access.path))].filter((path) => received.some((entry) => namesPath(entry.text, path)));
|
|
102
|
+
if (named.length > 0) {
|
|
103
|
+
const naming = received.find((entry) => named.some((path) => namesPath(entry.text, path)));
|
|
104
|
+
return [{
|
|
105
|
+
...base,
|
|
106
|
+
...(naming === undefined ? {} : { evidence: naming.evidence }),
|
|
107
|
+
strength: 'path',
|
|
108
|
+
paths: named.sort(),
|
|
109
|
+
writtenFrom,
|
|
110
|
+
...shown(written),
|
|
111
|
+
}];
|
|
112
|
+
}
|
|
113
|
+
// Nothing of the file was in what this record holds of the reply. Before that is said as a clean return, the
|
|
114
|
+
// one thing that can contradict it: a value only an agent below this delegation ever read, in the hands of the
|
|
115
|
+
// agent that delegated. A child's one channel to its parent is its reply, so the value crossed here - whatever
|
|
116
|
+
// carried it, and whether or not this version can read that carrier (`when-an-agent-finishes` D9). Saying
|
|
117
|
+
// "nothing came back" over that is the one answer the record rules out.
|
|
118
|
+
const crossed = filesOf(textsOf(model, delegation.parentAgentId, delegation.evidence.record));
|
|
119
|
+
if (crossed.paths.length > 0) {
|
|
120
|
+
return [{ ...base, strength: 'value', inferred: true, paths: crossed.paths, writtenFrom, ...shown(crossed) }];
|
|
121
|
+
}
|
|
122
|
+
const last = received.at(-1);
|
|
123
|
+
return [{ ...base, ...(last === undefined ? {} : { evidence: last.evidence }), strength: 'report', paths: [], writtenFrom, ...shown(written) }];
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Every string an agent wrote after a given record: its own messages, what it put into the calls it made, the later words
|
|
128
|
+
* it gave its helpers, and code or instructions of its own that are no call of the model (X14, X17).
|
|
129
|
+
*/
|
|
130
|
+
function textsOf(model, agentId, after) {
|
|
131
|
+
const strings = (value) => {
|
|
132
|
+
if (typeof value === 'string')
|
|
133
|
+
return [value];
|
|
134
|
+
if (Array.isArray(value))
|
|
135
|
+
return value.flatMap(strings);
|
|
136
|
+
if (value !== null && typeof value === 'object')
|
|
137
|
+
return Object.values(value).flatMap(strings);
|
|
138
|
+
return [];
|
|
139
|
+
};
|
|
140
|
+
return [
|
|
141
|
+
...model.messages.filter((message) => message.agentId === agentId && message.evidence.record > after).map((message) => message.text),
|
|
142
|
+
...model.events.filter((event) => event.agentId === agentId && event.evidence.record > after).flatMap((event) => strings(event.input)),
|
|
143
|
+
...model.delegations.flatMap((delegation) => delegation.followUps)
|
|
144
|
+
.filter((followUp) => followUp.senderAgentId === agentId && followUp.evidence.record > after)
|
|
145
|
+
.map((followUp) => followUp.text),
|
|
146
|
+
...model.contexts.filter((context) => context.author === 'agent' && context.agentId === agentId && context.evidence.record > after)
|
|
147
|
+
.map((context) => context.text),
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
/** The agent and every agent a chain of delegations below it started. */
|
|
151
|
+
function agentsFrom(model, agentId) {
|
|
152
|
+
const below = new Set([agentId]);
|
|
153
|
+
let grew = true;
|
|
154
|
+
while (grew) {
|
|
155
|
+
grew = false;
|
|
156
|
+
for (const delegation of model.delegations) {
|
|
157
|
+
const child = delegation.childAgentId;
|
|
158
|
+
if (child !== undefined && below.has(delegation.parentAgentId) && !below.has(child)) {
|
|
159
|
+
below.add(child);
|
|
160
|
+
grew = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return below;
|
|
165
|
+
}
|
|
166
|
+
/** Whether the text names this path as a whole: `.env` inside `.env.local` is not it, `/abs/apps/web/.env` is. */
|
|
167
|
+
export function namesPath(text, path) {
|
|
168
|
+
for (let at = text.indexOf(path); at !== -1; at = text.indexOf(path, at + 1)) {
|
|
169
|
+
if (!/[\w.-]/.test(text.charAt(at - 1)) && !/[\w./-]/.test(text.charAt(at + path.length)))
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|