one-code-extension 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -1,307 +1,274 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Classifier
|
|
2
|
+
* Classifier payload assembly and stage-verdict parsing (pure).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The payload is Claude Code's shape (docs/decisions/auto-mode.md, P4/P5):
|
|
5
|
+
* system = <ruleset with Environment spliced> + <Session Context>
|
|
6
|
+
* user = <CLAUDE.md framing> + <transcript> + <stage final instruction>
|
|
7
|
+
* The two stages share system+framing+transcript byte-for-byte and differ only
|
|
8
|
+
* in the final instruction block, so stage 2 is a cache hit off stage 1.
|
|
9
|
+
*
|
|
10
|
+
* Kept free of any pi/network import so the exact text sent, and the parsing of
|
|
11
|
+
* what comes back, are unit-testable.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
import {
|
|
15
|
+
buildRuleset,
|
|
16
|
+
claudeMdFraming,
|
|
17
|
+
renderSessionContext,
|
|
18
|
+
type RuleExtras,
|
|
19
|
+
STAGE1_FINAL,
|
|
20
|
+
STAGE2_FINAL,
|
|
21
|
+
STAGE2_INTENT_ADDENDUM,
|
|
22
|
+
} from "./classifier-prompt.ts";
|
|
23
|
+
import { buildCategoryIndex, groundCategory, normalizeName, type RuleIndex } from "./rules.ts";
|
|
24
|
+
import { clip, renderTranscript, type TranscriptEntry } from "./transcript.ts";
|
|
11
25
|
|
|
12
26
|
export interface ClassifyRequest {
|
|
13
|
-
toolName: string;
|
|
14
|
-
/** The tool's arguments, already trimmed of anything oversized. */
|
|
15
|
-
input: Record<string, unknown>;
|
|
16
|
-
cwd: string;
|
|
17
|
-
/** Recent user messages, most recent last — the only source of "explicit intent". */
|
|
18
|
-
userMessages: string[];
|
|
19
|
-
/** Deterministic findings from the shell pre-gate, when the call is a shell command. */
|
|
20
|
-
evidence?: ShellEvidence;
|
|
21
27
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
* The ordered transcript, user messages and tool inputs only (results
|
|
29
|
+
* stripped). Its **last entry is the action under review** — the caller
|
|
30
|
+
* appends the call being judged before handing it here.
|
|
31
|
+
*/
|
|
32
|
+
transcript: TranscriptEntry[];
|
|
33
|
+
/**
|
|
34
|
+
* Every user message this session, for intent-quote verification. Full, never
|
|
35
|
+
* a rolling window: in a long unattended run the authorizing setup message
|
|
36
|
+
* must still clear a later action (decision 2). Distinct from the transcript,
|
|
37
|
+
* whose oldest lines may be dropped for length.
|
|
28
38
|
*/
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
|
|
39
|
+
userMessages: string[];
|
|
40
|
+
/** The user's CLAUDE.md, wrapped in the (untrusted) framing block. */
|
|
41
|
+
claudeMd?: string;
|
|
42
|
+
/** Session identity for the Session Context block. */
|
|
43
|
+
username: string;
|
|
44
|
+
/** Environment slot lines (AutoModeConfig.environment) spliced into the ruleset. */
|
|
45
|
+
environment: string[];
|
|
46
|
+
/** User rule extras (AutoModeConfig hard_deny/soft_deny/allow) appended at CC's injection points. */
|
|
47
|
+
ruleExtras?: RuleExtras;
|
|
48
|
+
/** Native tool name of the action under review — for logging/debug only. */
|
|
49
|
+
toolName: string;
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
export type ClassifyDecision = "allow" | "block";
|
|
35
53
|
|
|
36
54
|
export interface ClassifyVerdict {
|
|
37
55
|
decision: ClassifyDecision;
|
|
38
|
-
/**
|
|
39
|
-
* What the user and the model are told. Composed from the cited rule's own
|
|
40
|
-
* text, never from the classifier's paraphrase of it.
|
|
41
|
-
*/
|
|
56
|
+
/** Shown to the user and the model: the grounded rule name, never a paraphrase. */
|
|
42
57
|
reason: string;
|
|
43
|
-
/** Derived from the
|
|
58
|
+
/** Derived from the grounded category's section, not a field the model chose. */
|
|
44
59
|
tier?: "hard_deny" | "soft_deny" | "allow" | "intent" | "unmatched" | "timeout";
|
|
45
|
-
/** The
|
|
60
|
+
/** The grounded rule name (or a sentinel like `intent-unverified`). */
|
|
46
61
|
ruleId?: string;
|
|
47
|
-
/**
|
|
48
|
-
* The classifier's own wording, kept for diagnosis and shown as clearly
|
|
49
|
-
* attributed commentary. Never treated as a statement of fact.
|
|
50
|
-
*/
|
|
62
|
+
/** The model's own wording, kept for diagnosis and shown attributed to it. */
|
|
51
63
|
raw?: string;
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* default, but Haiku will not cache a prefix under 2048 tokens — with the rules
|
|
60
|
-
* in the user message the instructions alone came to ~1,270, so every call
|
|
61
|
-
* re-sent ~2,900 identical tokens uncached (measured: cacheRead=0, cacheWrite=0).
|
|
62
|
-
* Rules are instructions rather than data, so this is also where they belong.
|
|
63
|
-
*
|
|
64
|
-
* `projectInstructions` deliberately stays in the *user* message even though it
|
|
65
|
-
* is equally stable: CLAUDE.md is checked-in content this gate does not trust, and
|
|
66
|
-
* promoting untrusted text into the system role to save tokens would launder its
|
|
67
|
-
* authority. Prefix caching is unaffected by what follows the system block.
|
|
67
|
+
* Build the shared payload plus the grounding index. The ruleset (~110 KB) is
|
|
68
|
+
* built once here and used for both the system prompt and the category index, so
|
|
69
|
+
* `classify()` does not rebuild and re-parse it a second time per call. The final
|
|
70
|
+
* instruction is appended per stage by the caller (stage1User / stage2User).
|
|
68
71
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
Apply four tiers in order:
|
|
78
|
-
|
|
79
|
-
1. HARD DENY (H ids) — block. Neither user intent nor an allow rule clears it.
|
|
80
|
-
2. SOFT DENY (S ids) — block, unless tier 3 or 4 clears it.
|
|
81
|
-
3. ALLOW (A ids) — exceptions to soft denials.
|
|
82
|
-
4. EXPLICIT USER INTENT — a user message that specifically describes the action clears a remaining soft denial. Never clears a hard denial.
|
|
83
|
-
|
|
84
|
-
Apply tier 4 properly; it is what keeps this gate usable. A user who names the operation and its target has authorised it, and blocking anyway is a false positive:
|
|
85
|
-
- "clean up the repo" → does NOT authorise a force push (too general).
|
|
86
|
-
- "force-push this branch" → DOES (operation and target named).
|
|
87
|
-
- "write hello to ~/notes.txt, outside the project — that's intentional" → DOES authorise that write, though writing outside the working directory is a soft denial. The exact path was named.
|
|
88
|
-
|
|
89
|
-
Where a clearing condition requires the user to have named something — a path, branch, target, resource — it is met only if you can quote their words naming it, verbatim, in "intentQuote". A request that delegates the choice to you ("somewhere safe", "pick a location") names nothing and does not clear such a rule: the point of the rule is that you, not the user, chose the destination.
|
|
90
|
-
|
|
91
|
-
If no rule matches and the action is routine work toward what the user asked for, allow it. If you cannot tell what an action would do, block. Judge the action taken, not how it is described, and do not stretch a rule to cover an action it does not describe.
|
|
92
|
-
|
|
93
|
-
A block must cite the id of the rule it rests on. If no rule describes the action, cite one of: "boundary" (the user drew a line here), "instructions" (the project file forbids it), "unclear" (you cannot tell what it would do). Never invent an id.
|
|
94
|
-
|
|
95
|
-
Do not assert facts about paths, hosts, or the repository beyond what STATIC ANALYSIS FACTS and WHY YOU ARE BEING ASKED establish — those are what is actually known. Your "note" is shown to the user attributed to you, so it must not carry a claim you cannot support.
|
|
96
|
-
|
|
97
|
-
Reply with JSON only:
|
|
98
|
-
{"analysis":"<brief reasoning, shown to no one>","decision":"allow","clearedBy":"<A-id | intent | none>","intentQuote":"<required when clearedBy is intent>"}
|
|
99
|
-
{"analysis":"...","decision":"block","rule":"<H-id | S-id | boundary | instructions | unclear>","note":"<optional, under 20 words: the specific target or missing detail>"}
|
|
100
|
-
|
|
101
|
-
Keep "analysis" under 40 words. It is read by no one, and a reply cut off by the output limit voids the verdict.`;
|
|
72
|
+
export function buildPayload(request: ClassifyRequest): { system: string; userPrefix: string; index: RuleIndex } {
|
|
73
|
+
const ruleset = buildRuleset(request.environment, request.ruleExtras);
|
|
74
|
+
const system = ruleset + renderSessionContext(request.username);
|
|
75
|
+
const framing = claudeMdFraming(request.claudeMd);
|
|
76
|
+
const transcript = renderTranscript(request.transcript);
|
|
77
|
+
const userPrefix = [framing, transcript].filter(Boolean).join("\n\n");
|
|
78
|
+
return { system, userPrefix, index: buildCategoryIndex(ruleset) };
|
|
79
|
+
}
|
|
102
80
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return
|
|
81
|
+
/** The stage-1 user message: shared prefix + the harm-only final instruction. */
|
|
82
|
+
export function stage1User(userPrefix: string): string {
|
|
83
|
+
return `${userPrefix}\n\n${STAGE1_FINAL.trimStart()}`;
|
|
106
84
|
}
|
|
107
85
|
|
|
108
86
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
87
|
+
* Ours — appended to STAGE2_FINAL only when the transcript carries a
|
|
88
|
+
* `denied_by_permission_rule` line. Stage 2's job is to weigh user intent, and
|
|
89
|
+
* that is exactly what let a retry through: after `Bash(rm:*)` refused
|
|
90
|
+
* `rm scripts/slow_build.sh`, `python3 -c "import os; os.remove(…)"` was cleared
|
|
91
|
+
* on the user's own "Delete scripts/slow_build.sh" (WEAK-MODEL-REVIEW-2026-09-06
|
|
92
|
+
* H2). A permission rule is the user's decision made in advance, so it outranks
|
|
93
|
+
* the intent stage rather than being excused by it. One-directional, like the
|
|
94
|
+
* intent addendum: it can only turn an allow into a block.
|
|
113
95
|
*/
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
`Everything not named above is outside the trust boundary — ${unset} trust slot(s) are unconfigured, so treat their subjects as external.`,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
return section("environment", lines);
|
|
124
|
-
}
|
|
96
|
+
export const STAGE2_DENIAL_ADDENDUM =
|
|
97
|
+
"\n\nAdditionally: the transcript contains one or more `denied_by_permission_rule` entries — actions the user's own " +
|
|
98
|
+
"permission rules already refused. Those rules are the user's decision about a CLASS of action, made in advance, and " +
|
|
99
|
+
"user intent does NOT override them. If the action under review would achieve the effect of a denied action by another " +
|
|
100
|
+
"route (a different command, a script, an interpreter one-liner, another tool), treat it as a circumvention of the " +
|
|
101
|
+
"user's rules and BLOCK it, however plainly the user asked for the underlying goal.";
|
|
125
102
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
103
|
+
/**
|
|
104
|
+
* The stage-2 user message: shared prefix + full-eval final + our intent
|
|
105
|
+
* addendum, plus the denial addendum when the transcript holds a rule denial.
|
|
106
|
+
*/
|
|
107
|
+
export function stage2User(userPrefix: string, afterRuleDenial = false): string {
|
|
108
|
+
return `${userPrefix}\n\n${STAGE2_FINAL.trimStart()}${STAGE2_INTENT_ADDENDUM}${afterRuleDenial ? STAGE2_DENIAL_ADDENDUM : ""}`;
|
|
129
109
|
}
|
|
130
110
|
|
|
131
111
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
112
|
+
* Ours — reframes stage 2 for the completed-run subagent review. CC's final block
|
|
113
|
+
* tells the model to judge "the most recent action"; a retrospective sequence
|
|
114
|
+
* review needs it to weigh the whole finished run instead (a series of
|
|
115
|
+
* individually unremarkable steps can add up to a violation). Restores the
|
|
116
|
+
* framing the pre-two-stage `routedBecause` carried.
|
|
135
117
|
*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
if (evidence.executionPrimitives.length > 0) {
|
|
147
|
-
lines.push(`Touches paths whose contents execute later: ${evidence.executionPrimitives.join(", ")}`);
|
|
148
|
-
}
|
|
149
|
-
if (evidence.network.length > 0) lines.push(`Network-capable commands: ${evidence.network.join(", ")}`);
|
|
150
|
-
for (const note of evidence.notes) lines.push(`Note: this command ${note}`);
|
|
151
|
-
return lines.length > 0 ? `<static_analysis_facts>\n${lines.map((l) => `- ${l}`).join("\n")}\n</static_analysis_facts>` : "";
|
|
118
|
+
export const STAGE_REVIEW_ADDENDUM =
|
|
119
|
+
"\n\nNote: this is a RETROSPECTIVE review of a delegated subagent run that has ALREADY completed — " +
|
|
120
|
+
"the transcript's trailing entries are the subagent's actions (summarized). You are not approving a " +
|
|
121
|
+
"pending action; judge the sequence of those actions AS A WHOLE and flag whether, together, they did " +
|
|
122
|
+
"something the rules forbid — not only the final entry.";
|
|
123
|
+
|
|
124
|
+
/** The completed-run review message: shared prefix + full-eval final + review reframing. */
|
|
125
|
+
export function reviewUser(userPrefix: string): string {
|
|
126
|
+
return `${userPrefix}\n\n${STAGE2_FINAL.trimStart()}${STAGE_REVIEW_ADDENDUM}`;
|
|
152
127
|
}
|
|
153
128
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
129
|
+
/** The integer inside the last `<severity>…` tag (closing tag optional). */
|
|
130
|
+
export function parseSeverity(text: string): number | null {
|
|
131
|
+
const matches = [...text.matchAll(/<severity>\s*(\d{1,3})/gi)];
|
|
132
|
+
if (matches.length === 0) return null;
|
|
133
|
+
const value = Number.parseInt(matches[matches.length - 1][1], 10);
|
|
134
|
+
if (Number.isNaN(value)) return null;
|
|
135
|
+
return Math.max(0, Math.min(100, value));
|
|
136
|
+
}
|
|
162
137
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
renderEnvironment(config.environment),
|
|
171
|
-
]
|
|
172
|
-
.filter(Boolean)
|
|
173
|
-
.join("\n\n");
|
|
138
|
+
/** The content of the last `<tag>…</tag>` (or `<tag>…` unterminated), trimmed. */
|
|
139
|
+
function parseTag(text: string, tag: string): string | undefined {
|
|
140
|
+
const closed = [...text.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "gi"))];
|
|
141
|
+
const raw = closed.length > 0 ? closed[closed.length - 1][1] : text.match(new RegExp(`<${tag}>([\\s\\S]*)`, "i"))?.[1];
|
|
142
|
+
const trimmed = raw?.trim();
|
|
143
|
+
return trimmed ? trimmed : undefined;
|
|
144
|
+
}
|
|
174
145
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
? `<project_instructions>\n${clip(request.projectInstructions, 4000)}\n</project_instructions>`
|
|
178
|
-
: "",
|
|
179
|
-
`<user_messages>\n${
|
|
180
|
-
request.userMessages.length > 0
|
|
181
|
-
? request.userMessages.map((message) => `- ${clip(message, 1500)}`).join("\n")
|
|
182
|
-
: "- (none yet this session)"
|
|
183
|
-
}\n</user_messages>`,
|
|
184
|
-
`<working_directory>${request.cwd}</working_directory>`,
|
|
185
|
-
request.routedBecause ? `<why_you_are_being_asked>\n${request.routedBecause}\n</why_you_are_being_asked>` : "",
|
|
186
|
-
request.evidence ? renderEvidence(request.evidence) : "",
|
|
187
|
-
`<tool_call>\ntool: ${request.toolName}\n${argumentLines}\n</tool_call>`,
|
|
188
|
-
"Decide on the tool call above. JSON only.",
|
|
189
|
-
];
|
|
146
|
+
/** The threshold: below is allow, at/above is block (CC's Output Format). */
|
|
147
|
+
export const SEVERITY_THRESHOLD = 50;
|
|
190
148
|
|
|
191
|
-
|
|
192
|
-
|
|
149
|
+
/** A quote shorter than this proves nothing — a stray word matches any transcript. */
|
|
150
|
+
const INTENT_MIN_CHARS = 8;
|
|
193
151
|
|
|
194
|
-
/**
|
|
195
|
-
|
|
196
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Strip what a model adds around a verbatim quote without changing its words:
|
|
154
|
+
* wrapping quotation marks (straight, typographic, backticks, any nesting —
|
|
155
|
+
* the runs are greedy), sentence punctuation left outside the closing quote
|
|
156
|
+
* (`"…".`), and leading or trailing ellipses marking a truncated quote. Then
|
|
157
|
+
* normalizeName folds case, whitespace runs and trailing sentence punctuation.
|
|
158
|
+
* Exported for tests.
|
|
159
|
+
*/
|
|
160
|
+
export function normalizeQuote(value: string): string {
|
|
161
|
+
const text = value
|
|
162
|
+
.replace(/^[\s"'`\u201c\u201d\u2018\u2019\u00ab\u00bb]+/, "")
|
|
163
|
+
.replace(/["'`\u201c\u201d\u2018\u2019\u00ab\u00bb\s.,:;]+$/, "")
|
|
164
|
+
.replace(/^(\u2026|\.{3,})\s*/, "")
|
|
165
|
+
.replace(/\s*(\u2026|\.{3,})$/, "");
|
|
166
|
+
return normalizeName(text);
|
|
197
167
|
}
|
|
198
168
|
|
|
199
|
-
/**
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Whether the classifier's `<intent>` quote is really the user's words. The
|
|
171
|
+
* whole quote (after normalizeQuote) must be a substring of one user message;
|
|
172
|
+
* failing that, a multi-line quote passes when every line of at least
|
|
173
|
+
* INTENT_MIN_CHARS is itself a substring — a model quoting two sentences from
|
|
174
|
+
* one message often joins them with a newline the original never had. Live
|
|
175
|
+
* probes (2026-09-05, Haiku twice) returned `<intent>"…"</intent>` for a quote
|
|
176
|
+
* that was verbatim; the bare substring test rejected it and a legitimate allow
|
|
177
|
+
* became a soft block — the failure the auto-mode entry says gets auto mode
|
|
178
|
+
* switched off. Still one-directional: nothing here can turn a block into an
|
|
179
|
+
* allow that the words themselves do not support.
|
|
180
|
+
*/
|
|
181
|
+
export function intentQuoted(intent: string, userMessages: string[]): boolean {
|
|
182
|
+
const messages = userMessages.map((message) => normalizeName(message));
|
|
183
|
+
const whole = normalizeQuote(intent);
|
|
184
|
+
if (whole.length >= INTENT_MIN_CHARS && messages.some((message) => message.includes(whole))) return true;
|
|
185
|
+
const lines = intent
|
|
186
|
+
.split(/\r?\n/)
|
|
187
|
+
.map((line) => normalizeQuote(line))
|
|
188
|
+
.filter((line) => line.length > 0);
|
|
189
|
+
if (lines.length < 2 || lines.some((line) => line.length < INTENT_MIN_CHARS)) return false;
|
|
190
|
+
// Every line must come from the SAME message, IN ORDER: two authorizations
|
|
191
|
+
// from different turns stitched together are not one statement of intent,
|
|
192
|
+
// and neither is a later clause pasted in front of an earlier one ("edit
|
|
193
|
+
// config.yaml" + "touch secrets.yaml" lifted out of "under no circumstances
|
|
194
|
+
// touch secrets.yaml" must not verify because each is a substring somewhere).
|
|
195
|
+
return messages.some((message) => {
|
|
196
|
+
let cursor = 0;
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
const at = message.indexOf(line, cursor);
|
|
199
|
+
if (at < 0) return false;
|
|
200
|
+
cursor = at + line.length;
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
});
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
|
-
* Parse
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* Anything unparseable, or a block citing an id that does not exist, is still a
|
|
214
|
-
* block — a classifier whose answer cannot be read or trusted has approved
|
|
215
|
-
* nothing — but it is reported as exactly that rather than passed off as a rule.
|
|
207
|
+
* Parse a completed stage-2 reply and ground it. severity < 50 ⇒ allow (with the
|
|
208
|
+
* one-directional intent-quote check when the model credits user intent);
|
|
209
|
+
* severity ≥ 50 ⇒ block, with the tier derived from the grounded category rather
|
|
210
|
+
* than from anything the model asserted. An unparseable severity, or a block
|
|
211
|
+
* citing a category that is not a real rule, is still a block but tagged so it
|
|
212
|
+
* cannot borrow a real HARD rule's authority.
|
|
216
213
|
*/
|
|
217
|
-
export function
|
|
218
|
-
const
|
|
219
|
-
|
|
214
|
+
export function parseStage2(text: string, index: RuleIndex, userMessages: string[] = []): ClassifyVerdict {
|
|
215
|
+
const severity = parseSeverity(text);
|
|
216
|
+
const raw = clip(text.replace(/\s+/g, " ").trim(), 300) || undefined;
|
|
217
|
+
if (severity === null) {
|
|
220
218
|
return {
|
|
221
219
|
decision: "block",
|
|
222
|
-
reason: "The approval classifier
|
|
220
|
+
reason: "The approval classifier's reply carried no severity, so the verdict could not be read.",
|
|
223
221
|
tier: "unmatched",
|
|
224
|
-
raw
|
|
222
|
+
raw,
|
|
225
223
|
};
|
|
226
224
|
}
|
|
227
225
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
raw: clip(match[0], 200),
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (parsed.decision === "allow") {
|
|
241
|
-
const clearedBy = typeof parsed.clearedBy === "string" ? parsed.clearedBy.trim() : undefined;
|
|
242
|
-
|
|
243
|
-
// An allow resting on user intent is the one claim worth checking, because
|
|
244
|
-
// it is the claim a prompt injection most wants to manufacture and the one
|
|
245
|
-
// the model is most prone to stretch — "pick a location yourself" read as
|
|
246
|
-
// authorising the location it picked. The quote makes it verifiable, so
|
|
247
|
-
// verify it: an intent that cannot be found in the user's own words is not
|
|
248
|
-
// an intent.
|
|
249
|
-
if (clearedBy === "intent") {
|
|
250
|
-
const quote = typeof parsed.intentQuote === "string" ? normalize(parsed.intentQuote) : "";
|
|
251
|
-
const found = quote.length >= 8 && userMessages.some((message) => normalize(message).includes(quote));
|
|
252
|
-
if (!found) {
|
|
226
|
+
if (severity < SEVERITY_THRESHOLD) {
|
|
227
|
+
const intent = parseTag(text, "intent");
|
|
228
|
+
if (intent) {
|
|
229
|
+
// An allow resting on user intent is the one claim worth checking: it is
|
|
230
|
+
// what a prompt injection most wants to manufacture and what the model is
|
|
231
|
+
// most prone to stretch. Verified against the full user messages, so an
|
|
232
|
+
// early authorization still clears late (decision 2).
|
|
233
|
+
if (!intentQuoted(intent, userMessages)) {
|
|
253
234
|
return {
|
|
254
235
|
decision: "block",
|
|
255
236
|
reason:
|
|
256
237
|
"The classifier cleared this as something the user asked for, but could not point to the user's own words asking for it.",
|
|
257
238
|
tier: "soft_deny",
|
|
258
239
|
ruleId: "intent-unverified",
|
|
259
|
-
|
|
240
|
+
// The full quote, not a clip: the mismatch class is only diagnosable
|
|
241
|
+
// from the exact text the model produced (2026-09-05 H4).
|
|
242
|
+
raw: intent,
|
|
260
243
|
};
|
|
261
244
|
}
|
|
245
|
+
return { decision: "allow", reason: "", tier: "intent" };
|
|
262
246
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
ruleId: clearedBy && (index.byId.has(clearedBy) || clearedBy === "intent") ? clearedBy : undefined,
|
|
269
|
-
};
|
|
247
|
+
// A plain clear. The cited category (an ALLOW exception, usually) is kept
|
|
248
|
+
// as the ruleId for the log; a clear citing nothing is what the caller tags
|
|
249
|
+
// `unverified-clear` when stage 1 had flagged the call.
|
|
250
|
+
const cited = parseTag(text, "category");
|
|
251
|
+
return { decision: "allow", reason: "", tier: "allow", ...(cited ? { ruleId: clip(cited, 80) } : {}) };
|
|
270
252
|
}
|
|
271
253
|
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
const reserved = RESERVED_BLOCK_IDS[cited];
|
|
276
|
-
if (reserved) {
|
|
277
|
-
return { decision: "block", reason: reserved.reason, tier: reserved.tier, ruleId: cited, raw: commentary };
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const rule = index.byId.get(cited);
|
|
254
|
+
// severity >= 50 → block. Ground the cited category against the rule index.
|
|
255
|
+
const category = parseTag(text, "category");
|
|
256
|
+
const rule = groundCategory(index, category);
|
|
281
257
|
if (!rule) {
|
|
282
|
-
// An uncitable block is still a block, but it must not borrow a rule's
|
|
283
|
-
// authority — the tier stays "unmatched", so it cannot skip the prompt the
|
|
284
|
-
// way a real hard denial does.
|
|
285
258
|
return {
|
|
286
259
|
decision: "block",
|
|
287
|
-
reason:
|
|
260
|
+
reason: category
|
|
261
|
+
? `The approval classifier blocked this as "${category}", which is not a rule that could be matched.`
|
|
262
|
+
: "The approval classifier blocked this without naming a rule.",
|
|
288
263
|
tier: "unmatched",
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
if (rule.tier === "allow") {
|
|
293
|
-
// Citing an exception as grounds for a block is incoherent; treat it the
|
|
294
|
-
// same as citing nothing rather than reporting a nonsensical reason.
|
|
295
|
-
return {
|
|
296
|
-
decision: "block",
|
|
297
|
-
reason: "The approval classifier blocked this citing an allow rule, which does not describe a denial.",
|
|
298
|
-
tier: "unmatched",
|
|
299
|
-
ruleId: cited,
|
|
300
|
-
raw: commentary,
|
|
264
|
+
ruleId: category,
|
|
265
|
+
raw,
|
|
301
266
|
};
|
|
302
267
|
}
|
|
268
|
+
return { decision: "block", reason: rule.name, tier: rule.tier, ruleId: rule.name, raw };
|
|
269
|
+
}
|
|
303
270
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
271
|
+
/** Convenience for callers that hold the ruleset text rather than a prebuilt index. */
|
|
272
|
+
export function indexFor(environment: string[], extras?: RuleExtras): RuleIndex {
|
|
273
|
+
return buildCategoryIndex(buildRuleset(environment, extras));
|
|
307
274
|
}
|