@visual-z/xeno 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2066
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1713
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3001
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -200
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/screen.ts +0 -548
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
package/src/core/permissions.ts
DELETED
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure permission rules: which tools the model may see, and whether a
|
|
3
|
-
* call is allow / ask / deny.
|
|
4
|
-
*
|
|
5
|
-
* Approval is the last look at what is about to run, not a boundary.
|
|
6
|
-
* Path tools are already confined to the workspace root; host commands
|
|
7
|
-
* are not, which is why shell entry points default to ask.
|
|
8
|
-
*
|
|
9
|
-
* This module must stay free of I/O, clocks, and randomness. The CLI
|
|
10
|
-
* compiles flags and session memory into a rule list; this file only
|
|
11
|
-
* matches them. There is no command-danger heuristic: a screen that
|
|
12
|
-
* misses `find . -delete` is worse than none, because it implies
|
|
13
|
-
* someone is checking.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export type PermissionAction = "allow" | "ask" | "deny";
|
|
17
|
-
|
|
18
|
-
export interface PermissionRequest {
|
|
19
|
-
readonly tool: string;
|
|
20
|
-
/** Exact command string for bash; omitted for tools that have none. */
|
|
21
|
-
readonly command?: string;
|
|
22
|
-
/** Normalized workspace-relative or external absolute file resources. */
|
|
23
|
-
readonly paths?: readonly string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface PermissionRule {
|
|
27
|
-
readonly action: PermissionAction;
|
|
28
|
-
/** Absent matches every tool. */
|
|
29
|
-
readonly tool?: string;
|
|
30
|
-
/** Absent matches every command. When set, matches only this exact string. */
|
|
31
|
-
readonly command?: string;
|
|
32
|
-
/** Literal argv prefix, matched only for a single command without expansion. */
|
|
33
|
-
readonly commandPrefix?: readonly string[];
|
|
34
|
-
readonly path?: string;
|
|
35
|
-
/** Portable path glob: *, ?, and **; no brace or character-class expansion. */
|
|
36
|
-
readonly pathGlob?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Who contributed the tool. `builtin` is the only producer today.
|
|
41
|
-
* `extension` is in the union so `--no-builtin-tools` can keep a
|
|
42
|
-
* non-builtin tool without a later schema change across layers.
|
|
43
|
-
*/
|
|
44
|
-
export type ToolSource = "builtin" | "extension";
|
|
45
|
-
|
|
46
|
-
/** Task capabilities are frozen once at session creation (O.7); not permissions. */
|
|
47
|
-
export const TOOL_PROFILES = {
|
|
48
|
-
// worktree joined the measured ten (2026-10-11, feat-worktree-tool): without
|
|
49
|
-
// it a model needing isolation had no checkout it was allowed to edit.
|
|
50
|
-
coding: ["read", "grep", "find", "write", "edit", "environment", "exec_command", "write_stdin", "history", "note", "worktree"],
|
|
51
|
-
foundation: ["read", "grep", "find", "write", "edit", "environment", "exec_command", "write_stdin", "history", "note", "worktree", "question", "apply_patch", "diff", "webfetch", "websearch", "format", "lsp"],
|
|
52
|
-
readonly: ["read", "grep", "find", "history", "note"],
|
|
53
|
-
research: ["read", "grep", "find", "history", "note", "branch", "resolve"],
|
|
54
|
-
data: ["read", "grep", "find", "history", "note", "query"],
|
|
55
|
-
full: null,
|
|
56
|
-
} as const;
|
|
57
|
-
export type ToolProfile = keyof typeof TOOL_PROFILES;
|
|
58
|
-
export function isToolProfile(value: string): value is ToolProfile {
|
|
59
|
-
return Object.hasOwn(TOOL_PROFILES, value);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface ToolSelection {
|
|
63
|
-
/** Applies only to builtin defaults; explicit only and extension capabilities win. */
|
|
64
|
-
readonly profile?: ToolProfile;
|
|
65
|
-
/** `--tools`: a strict allowlist. */
|
|
66
|
-
readonly only?: readonly string[];
|
|
67
|
-
/** `--exclude-tools`: removed after the allowlist (or the full set) is built. */
|
|
68
|
-
readonly exclude?: readonly string[];
|
|
69
|
-
/** `--no-tools`: start from nothing. Ignored when `only` is set. */
|
|
70
|
-
readonly none?: boolean;
|
|
71
|
-
/**
|
|
72
|
-
* `--no-builtin-tools`. Drops `source === "builtin"` (and untagged tools,
|
|
73
|
-
* which are builtin). Ignored when `only` is set: an explicit `--tools`
|
|
74
|
-
* names a specific tool and is a stronger statement than "not that category".
|
|
75
|
-
*/
|
|
76
|
-
readonly noBuiltin?: boolean;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* The flag a no-tty refusal names so a pipeline can choose explicitly.
|
|
81
|
-
*
|
|
82
|
-
* Session memory is not this flag: session memory dies with the process.
|
|
83
|
-
* A persisted "always allow" would fire months later when nobody is watching.
|
|
84
|
-
*/
|
|
85
|
-
export const ALLOW_TOOLS_FLAG = "--allow-tools";
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Product default. Shell entry points ask because their effects are not
|
|
89
|
-
* confined to the workspace root; path tools keep their own confinement.
|
|
90
|
-
*/
|
|
91
|
-
export function defaultRules(): readonly PermissionRule[] {
|
|
92
|
-
return [
|
|
93
|
-
// Templates are ordinary source; actual environment values deserve the
|
|
94
|
-
// same last look as commands. Indirect readers also check read resources.
|
|
95
|
-
{ action: "allow", tool: "read", pathGlob: "**/.env.example" },
|
|
96
|
-
{ action: "ask", tool: "read", pathGlob: "**/.env" },
|
|
97
|
-
{ action: "ask", tool: "read", pathGlob: "**/.env.*" },
|
|
98
|
-
{ action: "ask", tool: "bash" },
|
|
99
|
-
// Same reach as bash. write_stdin needs no rule of its own: it can only
|
|
100
|
-
// address a session exec_command already started, under this approval.
|
|
101
|
-
{ action: "ask", tool: "exec_command" },
|
|
102
|
-
{ action: "ask", tool: "environment" },
|
|
103
|
-
{ action: "ask", tool: "target" },
|
|
104
|
-
{ action: "allow" },
|
|
105
|
-
];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* CLI opt-ins compiled to one process-scoped rule list. YOLO permits every
|
|
110
|
-
* tool; capability admission and path confinement still apply (D13).
|
|
111
|
-
* Neither this flag nor remembered approvals become a persisted policy.
|
|
112
|
-
*/
|
|
113
|
-
export function rulesWithAllowTools(
|
|
114
|
-
allowTools: readonly string[],
|
|
115
|
-
options: { readonly yolo?: boolean } = {},
|
|
116
|
-
): PermissionRule[] {
|
|
117
|
-
if (options.yolo === true) return [{ action: "allow" }];
|
|
118
|
-
return [
|
|
119
|
-
...allowTools.map((tool) => ({ action: "allow" as const, tool })),
|
|
120
|
-
...defaultRules(),
|
|
121
|
-
];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function ruleMatches(rule: PermissionRule, request: PermissionRequest): boolean {
|
|
125
|
-
if (rule.tool !== undefined && rule.tool !== request.tool) return false;
|
|
126
|
-
if (rule.command !== undefined && rule.command !== request.command) return false;
|
|
127
|
-
if (rule.commandPrefix !== undefined) {
|
|
128
|
-
const words = request.command === undefined ? null : literalCommandWords(request.command);
|
|
129
|
-
if (!words || rule.commandPrefix.length === 0 || rule.commandPrefix.some((word, i) => words[i] !== word)) return false;
|
|
130
|
-
}
|
|
131
|
-
if (rule.path !== undefined && !request.paths?.some(path => normalizePermissionPath(path) === normalizePermissionPath(rule.path!))) return false;
|
|
132
|
-
if (rule.pathGlob !== undefined && !request.paths?.some(path => permissionPathMatches(rule.pathGlob!, path))) return false;
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* First matching rule wins. An empty list allows: the tool tests pass no
|
|
138
|
-
* rules because they are testing the tool, not the gate. The CLI always
|
|
139
|
-
* supplies the current process's compiled policy.
|
|
140
|
-
*/
|
|
141
|
-
export function decide(
|
|
142
|
-
rules: readonly PermissionRule[],
|
|
143
|
-
request: PermissionRequest,
|
|
144
|
-
): PermissionAction {
|
|
145
|
-
// A multi-file call needs every resource admitted. A broad allow for its
|
|
146
|
-
// first path must not conceal a denial on a later file (D13 / R13).
|
|
147
|
-
if (request.paths && request.paths.length > 1) {
|
|
148
|
-
const actions = request.paths.map(path => decide(rules, { ...request, paths: [path] }));
|
|
149
|
-
return actions.includes("deny") ? "deny" : actions.includes("ask") ? "ask" : "allow";
|
|
150
|
-
}
|
|
151
|
-
for (const rule of rules) {
|
|
152
|
-
if (ruleMatches(rule, request)) return rule.action;
|
|
153
|
-
}
|
|
154
|
-
return "allow";
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Configured denial is a ceiling; in-memory approval can only resolve asks. */
|
|
158
|
-
export function decideWithSessionRules(
|
|
159
|
-
configured: readonly PermissionRule[], remembered: readonly PermissionRule[], request: PermissionRequest,
|
|
160
|
-
): PermissionAction {
|
|
161
|
-
if (decide(configured, request) === "deny") return "deny";
|
|
162
|
-
return decide([...remembered, ...configured], request);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Where a gate outcome came from, for the durable approval record
|
|
167
|
-
* (feat-approval-events). `auto` is an allow by a rule with no selector --
|
|
168
|
-
* what `--yolo` compiles to -- so "no approval was required" is derived from
|
|
169
|
-
* the rule list itself rather than from a side flag. `none` means no rule
|
|
170
|
-
* matched and the empty-list default allowed: no policy was consulted.
|
|
171
|
-
*/
|
|
172
|
-
export type DecisionSource = "session_rule" | "config" | "auto" | "none";
|
|
173
|
-
export interface DecisionExplanation {
|
|
174
|
-
readonly action: PermissionAction;
|
|
175
|
-
readonly source: DecisionSource;
|
|
176
|
-
readonly rule?: PermissionRule;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Mirrors decide() exactly, but keeps the index of the rule that produced the action. */
|
|
180
|
-
function explainList(rules: readonly PermissionRule[], request: PermissionRequest): { action: PermissionAction; index?: number } {
|
|
181
|
-
if (request.paths && request.paths.length > 1) {
|
|
182
|
-
const each = request.paths.map(path => explainList(rules, { ...request, paths: [path] }));
|
|
183
|
-
const actions = each.map(item => item.action);
|
|
184
|
-
const action: PermissionAction = actions.includes("deny") ? "deny" : actions.includes("ask") ? "ask" : "allow";
|
|
185
|
-
return each.find(item => item.action === action)!;
|
|
186
|
-
}
|
|
187
|
-
const index = rules.findIndex(rule => ruleMatches(rule, request));
|
|
188
|
-
return index < 0 ? { action: "allow" } : { action: rules[index]!.action, index };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** decideWithSessionRules plus the rule that decided; the action is always identical (property-tested). */
|
|
192
|
-
export function explainDecision(
|
|
193
|
-
configured: readonly PermissionRule[], remembered: readonly PermissionRule[], request: PermissionRequest,
|
|
194
|
-
): DecisionExplanation {
|
|
195
|
-
const ceiling = explainList(configured, request);
|
|
196
|
-
if (ceiling.action === "deny") return { action: "deny", source: "config", rule: configured[ceiling.index!]! };
|
|
197
|
-
const combined = [...remembered, ...configured];
|
|
198
|
-
const found = explainList(combined, request);
|
|
199
|
-
if (found.index === undefined) return { action: found.action, source: "none" };
|
|
200
|
-
const rule = combined[found.index]!;
|
|
201
|
-
const unconditional = rule.tool === undefined && rule.command === undefined && rule.commandPrefix === undefined &&
|
|
202
|
-
rule.path === undefined && rule.pathGlob === undefined;
|
|
203
|
-
return { action: found.action, rule,
|
|
204
|
-
source: found.index < remembered.length ? "session_rule" : unconditional && rule.action === "allow" ? "auto" : "config" };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* A call needs a decision when the product default would not silently allow
|
|
209
|
-
* it, or when the effective policy did not. Calls outside both -- confined
|
|
210
|
-
* reads and edits under the default -- had nobody's judgement to record.
|
|
211
|
-
*/
|
|
212
|
-
export function approvalNeeded(request: PermissionRequest, effective: PermissionAction): boolean {
|
|
213
|
-
return effective !== "allow" || decide(defaultRules(), request) !== "allow";
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Stable one-line rule text for the log and the export; field order is fixed. */
|
|
217
|
-
export function describeRule(rule: PermissionRule): string {
|
|
218
|
-
return [rule.action, rule.tool ?? "*",
|
|
219
|
-
...(rule.command !== undefined ? [`command=${JSON.stringify(rule.command)}`] : []),
|
|
220
|
-
...(rule.commandPrefix !== undefined ? [`commandPrefix=${JSON.stringify(rule.commandPrefix.join(" "))}`] : []),
|
|
221
|
-
...(rule.path !== undefined ? [`path=${JSON.stringify(rule.path)}`] : []),
|
|
222
|
-
...(rule.pathGlob !== undefined ? [`pathGlob=${JSON.stringify(rule.pathGlob)}`] : [])].join(" ");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Lexical normalization is pure; the store adds canonical symlink resources. */
|
|
226
|
-
export function normalizePermissionPath(path: string): string {
|
|
227
|
-
const absolute = path.startsWith("/");
|
|
228
|
-
const parts: string[] = [];
|
|
229
|
-
for (const part of path.split("/")) {
|
|
230
|
-
if (!part || part === ".") continue;
|
|
231
|
-
if (part === ".." && parts.length > 0 && parts.at(-1) !== "..") parts.pop();
|
|
232
|
-
else if (part === ".." && absolute) continue;
|
|
233
|
-
else parts.push(part);
|
|
234
|
-
}
|
|
235
|
-
return (absolute ? "/" : "") + parts.join("/") || ".";
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** Dynamic programming avoids exponential regexp backtracking on model input. */
|
|
239
|
-
export function permissionPathMatches(pattern: string, path: string): boolean {
|
|
240
|
-
const glob = normalizePermissionPath(pattern), value = normalizePermissionPath(path);
|
|
241
|
-
let row = Array<boolean>(value.length + 1).fill(false); row[0] = true;
|
|
242
|
-
for (let i = 0; i < glob.length; i++) {
|
|
243
|
-
const char = glob[i]!;
|
|
244
|
-
const next = Array<boolean>(value.length + 1).fill(false);
|
|
245
|
-
if (char === "*") {
|
|
246
|
-
const deep = glob[i + 1] === "*";
|
|
247
|
-
if (deep) i++;
|
|
248
|
-
// **/ also matches zero directories, as in **/.env at the root.
|
|
249
|
-
const directories = deep && glob[i + 1] === "/";
|
|
250
|
-
if (directories) i++;
|
|
251
|
-
next[0] = row[0]!;
|
|
252
|
-
for (let j = 1; j <= value.length; j++) {
|
|
253
|
-
next[j] = row[j]! || ((deep || value[j - 1] !== "/") && next[j - 1]!);
|
|
254
|
-
if (directories && value[j - 1] !== "/") next[j] = row[j]!;
|
|
255
|
-
}
|
|
256
|
-
// A globstar followed by slash may consume complete directories only.
|
|
257
|
-
if (directories) {
|
|
258
|
-
let reachable = row[0]!;
|
|
259
|
-
for (let j = 1; j <= value.length; j++) {
|
|
260
|
-
reachable = reachable || row[j]!;
|
|
261
|
-
next[j] = row[j]! || (value[j - 1] === "/" && reachable);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
for (let j = 1; j <= value.length; j++) next[j] = row[j - 1]! &&
|
|
266
|
-
(char === "?" ? value[j - 1] !== "/" : char === value[j - 1]);
|
|
267
|
-
}
|
|
268
|
-
row = next;
|
|
269
|
-
}
|
|
270
|
-
return row[value.length]!;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* This is a prefix matcher, never a command-danger classifier. Unknown shell
|
|
275
|
-
* syntax fails to match and therefore retains the full-command ask (D13).
|
|
276
|
-
*/
|
|
277
|
-
export function literalCommandWords(command: string): string[] | null {
|
|
278
|
-
const words: string[] = [];
|
|
279
|
-
let word = "", started = false, quote: "'" | '"' | null = null;
|
|
280
|
-
for (let i = 0; i < command.length; i++) {
|
|
281
|
-
const char = command[i]!;
|
|
282
|
-
if (char === "\n" || char === "\r" || char === "\0") return null;
|
|
283
|
-
if (quote === "'") {
|
|
284
|
-
if (char === "'") quote = null; else word += char;
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
if (char === "\\") {
|
|
288
|
-
const escaped = command[++i];
|
|
289
|
-
if (escaped === undefined || escaped === "\n" || escaped === "\r") return null;
|
|
290
|
-
// Bash double quotes retain backslash before ordinary characters.
|
|
291
|
-
word += quote === '"' && !['$', '`', '"', "\\"].includes(escaped) ? `\\${escaped}` : escaped;
|
|
292
|
-
started = true; continue;
|
|
293
|
-
}
|
|
294
|
-
if (quote === '"') {
|
|
295
|
-
if (char === '"') quote = null;
|
|
296
|
-
else if (char === "$" || char === "`") return null;
|
|
297
|
-
else word += char;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
if (char === "'" || char === '"') { quote = char; started = true; continue; }
|
|
301
|
-
if (char === " " || char === "\t") { if (started) words.push(word); word = ""; started = false; continue; }
|
|
302
|
-
if (";|&<>($`*?[]{}~#".includes(char)) return null;
|
|
303
|
-
word += char; started = true;
|
|
304
|
-
}
|
|
305
|
-
if (quote !== null) return null;
|
|
306
|
-
if (started) words.push(word);
|
|
307
|
-
return words.length ? words : null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** Unknown config keys are errors: a typo must not silently weaken a policy. */
|
|
311
|
-
export function parsePermissionRule(value: unknown): PermissionRule {
|
|
312
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw Error("permission rule must be an object");
|
|
313
|
-
const rule = value as Record<string, unknown>;
|
|
314
|
-
if (Object.keys(rule).some(key => !["action", "tool", "command", "commandPrefix", "path", "pathGlob"].includes(key))) throw Error("unknown permission rule field");
|
|
315
|
-
if (!["allow", "ask", "deny"].includes(String(rule["action"]))) throw Error("permission rule action must be allow, ask or deny");
|
|
316
|
-
for (const key of ["tool", "command", "path", "pathGlob"]) {
|
|
317
|
-
if (rule[key] !== undefined && (typeof rule[key] !== "string" || !String(rule[key]).trim() || String(rule[key]).includes("\0"))) throw Error(`permission ${key} must be a nonempty string`);
|
|
318
|
-
}
|
|
319
|
-
if (rule["path"] !== undefined && rule["pathGlob"] !== undefined) throw Error("permission path and pathGlob are mutually exclusive");
|
|
320
|
-
if (typeof rule["pathGlob"] === "string" && /[\[\]{}]/.test(rule["pathGlob"])) throw Error("permission pathGlob supports only *, ? and **");
|
|
321
|
-
const prefix = rule["commandPrefix"];
|
|
322
|
-
if (prefix !== undefined && (!Array.isArray(prefix) || prefix.length === 0 || prefix.some(word => typeof word !== "string" || !word || /[\r\n\0]/.test(word)))) throw Error("permission commandPrefix must contain literal argv tokens");
|
|
323
|
-
if (prefix !== undefined && rule["command"] !== undefined) throw Error("permission command and commandPrefix are mutually exclusive");
|
|
324
|
-
return structuredClone(value) as PermissionRule;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Comma-separated flag values. Empty tokens are dropped so `--tools a,,b`
|
|
329
|
-
* does not invent a blank name.
|
|
330
|
-
*/
|
|
331
|
-
export function parseToolList(raw: string): string[] {
|
|
332
|
-
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Names the model will see. An excluded tool has to vanish here -- leaving
|
|
337
|
-
* it in the schema and failing at call time teaches the model to retry
|
|
338
|
-
* the same call and burn tokens.
|
|
339
|
-
*
|
|
340
|
-
* `--tools` is a strict allowlist. `--no-tools` starts from nothing, then
|
|
341
|
-
* `--tools` can add names back (pi's combination). `--exclude-tools` is
|
|
342
|
-
* applied last.
|
|
343
|
-
*/
|
|
344
|
-
export function selectToolNames(
|
|
345
|
-
available: readonly string[],
|
|
346
|
-
selection: ToolSelection,
|
|
347
|
-
): string[] {
|
|
348
|
-
let names: string[];
|
|
349
|
-
if (selection.only !== undefined) {
|
|
350
|
-
const allow = new Set(selection.only);
|
|
351
|
-
names = available.filter((n) => allow.has(n));
|
|
352
|
-
} else if (selection.none === true) {
|
|
353
|
-
names = [];
|
|
354
|
-
} else {
|
|
355
|
-
names = [...available];
|
|
356
|
-
}
|
|
357
|
-
if (selection.exclude !== undefined) {
|
|
358
|
-
const ex = new Set(selection.exclude);
|
|
359
|
-
names = names.filter((n) => !ex.has(n));
|
|
360
|
-
}
|
|
361
|
-
return names;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export function selectTools<T extends { name: string; source?: ToolSource }>(
|
|
365
|
-
tools: readonly T[],
|
|
366
|
-
selection: ToolSelection,
|
|
367
|
-
): T[] {
|
|
368
|
-
const names = new Set(selectToolNames(tools.map((t) => t.name), selection));
|
|
369
|
-
let selected = tools.filter((t) => names.has(t.name));
|
|
370
|
-
const profile = selection.profile === undefined ? null : TOOL_PROFILES[selection.profile];
|
|
371
|
-
if (profile !== null && selection.only === undefined) {
|
|
372
|
-
const defaults = new Set<string>(profile);
|
|
373
|
-
selected = selected.filter(t => t.source === "extension" || defaults.has(t.name));
|
|
374
|
-
}
|
|
375
|
-
// `--tools` already named specific tools, so the category filter does
|
|
376
|
-
// not get a second veto. Untagged tools are builtin: that is today's
|
|
377
|
-
// only producer, and treating absence as "keep" would make the flag a
|
|
378
|
-
// no-op until every schema is recut.
|
|
379
|
-
if (selection.noBuiltin === true && selection.only === undefined) {
|
|
380
|
-
selected = selected.filter((t) => (t.source ?? "builtin") !== "builtin");
|
|
381
|
-
}
|
|
382
|
-
return selected;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export function permissionRequest(name: string, input: unknown): PermissionRequest {
|
|
386
|
-
if (typeof input !== "object" || input === null) {
|
|
387
|
-
return { tool: name };
|
|
388
|
-
}
|
|
389
|
-
const fields = input as { readonly [k: string]: unknown };
|
|
390
|
-
const command = ["bash", "exec_command", "environment"].includes(name) ? fields[name === "exec_command" ? "cmd" : "command"] : undefined;
|
|
391
|
-
const paths = typeof fields["path"] === "string" ? [fields["path"]]
|
|
392
|
-
: Array.isArray(fields["paths"]) && fields["paths"].every(path => typeof path === "string") ? fields["paths"] as string[] : undefined;
|
|
393
|
-
return { tool: name, ...(typeof command === "string" ? { command } : {}),
|
|
394
|
-
...(paths ? { paths: paths.map(normalizePermissionPath) } : {}) };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Why this ask became a deny: nobody could be asked. Names the flag that
|
|
399
|
-
* would have allowed the call without asking, so a CI author can choose.
|
|
400
|
-
*/
|
|
401
|
-
export function askWithoutTtyMessage(request: PermissionRequest): string {
|
|
402
|
-
return (
|
|
403
|
-
`refused: ${request.tool} requires approval and there is no tty to ask. ` +
|
|
404
|
-
`Pass ${ALLOW_TOOLS_FLAG} ${request.tool} to allow it without asking.`
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
export function deniedMessage(request: PermissionRequest): string {
|
|
409
|
-
return `refused: ${request.tool} was denied`;
|
|
410
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/** R7 / D8 / D11: an observed process and a live control capability are distinct facts. */
|
|
2
|
-
import type { ArtifactRef, LogEvent } from "./events.ts";
|
|
3
|
-
import { isObservationArtifact } from "./observation-view.ts";
|
|
4
|
-
import { isCommandArtifactReceipt, type CommandArtifactReceipt } from "./command-artifacts.ts";
|
|
5
|
-
|
|
6
|
-
export type ProcessSource = { host: string; workspace: string; cwd: string; runtime: string };
|
|
7
|
-
export type ProcessReceipt = {
|
|
8
|
-
kind: "process";
|
|
9
|
-
version: 1;
|
|
10
|
-
session_id: number;
|
|
11
|
-
state: "running" | "exited" | "cancelled" | "timeout" | "unknown";
|
|
12
|
-
capability: "live" | "closed" | "lost" | "expired" | "unknown";
|
|
13
|
-
/** Legacy running/exit fields keep their meaning; null explicitly means no observation. */
|
|
14
|
-
running: boolean | null;
|
|
15
|
-
exit: number | null;
|
|
16
|
-
source: ProcessSource | null;
|
|
17
|
-
output?: ArtifactRef;
|
|
18
|
-
full_output?: ArtifactRef;
|
|
19
|
-
/** Half-open raw stream byte offsets covered by output, not preview character positions. */
|
|
20
|
-
cursor?: { unit: "bytes"; start: number; end: number };
|
|
21
|
-
replayed: boolean;
|
|
22
|
-
/** Complete means stream EOF was observed; full_output always means all captured bytes. */
|
|
23
|
-
capture?: "complete" | "partial";
|
|
24
|
-
/** Same one-shot command observation, not a new scan at each poll. */
|
|
25
|
-
artifacts?: CommandArtifactReceipt;
|
|
26
|
-
origin?: { node: number; event: string };
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
30
|
-
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
31
|
-
export function isProcessReceipt(value: unknown): value is ProcessReceipt {
|
|
32
|
-
if (!object(value) || value["kind"] !== "process" || value["version"] !== 1 || !natural(value["session_id"]) || value["session_id"] < 1 ||
|
|
33
|
-
!["running", "exited", "cancelled", "timeout", "unknown"].includes(String(value["state"])) ||
|
|
34
|
-
!["live", "closed", "lost", "expired", "unknown"].includes(String(value["capability"])) || typeof value["replayed"] !== "boolean") return false;
|
|
35
|
-
if (value["state"] === "running") {
|
|
36
|
-
if (value["running"] !== true || value["exit"] !== null || !["live", "lost"].includes(String(value["capability"]))) return false;
|
|
37
|
-
} else if (value["state"] === "unknown") {
|
|
38
|
-
if (value["running"] !== null || value["exit"] !== null || value["capability"] !== "unknown") return false;
|
|
39
|
-
} else if (value["running"] !== false || !Number.isInteger(value["exit"]) || !["closed", "expired"].includes(String(value["capability"]))) return false;
|
|
40
|
-
const source = value["source"];
|
|
41
|
-
if (["lost", "expired"].includes(String(value["capability"])) && value["replayed"] !== true) return false;
|
|
42
|
-
if (value["capture"] !== undefined && !["complete", "partial"].includes(String(value["capture"]))) return false;
|
|
43
|
-
const artifacts = value["artifacts"];
|
|
44
|
-
if (artifacts !== undefined && (!isCommandArtifactReceipt(artifacts) || artifacts.exit !== value["exit"] ||
|
|
45
|
-
value["state"] === "unknown" || !object(value["source"]) || !["host", "workspace", "cwd", "runtime"].every(key =>
|
|
46
|
-
(artifacts.source as Record<string, string>)[key] === (value["source"] as Record<string, unknown>)[key]))) return false;
|
|
47
|
-
if (source !== null && (!object(source) || !["host", "workspace", "cwd", "runtime"].every(key => typeof source[key] === "string" && source[key] !== ""))) return false;
|
|
48
|
-
for (const name of ["output", "full_output"]) if (value[name] !== undefined && !isObservationArtifact(value[name])) return false;
|
|
49
|
-
const cursor = value["cursor"];
|
|
50
|
-
if (cursor !== undefined && (!object(cursor) || cursor["unit"] !== "bytes" || !natural(cursor["start"]) || !natural(cursor["end"]) ||
|
|
51
|
-
cursor["end"] < cursor["start"] || !isObservationArtifact(value["output"]) || value["output"].bytes !== cursor["end"] - cursor["start"])) return false;
|
|
52
|
-
if (isObservationArtifact(value["full_output"]) && object(cursor) && typeof cursor["end"] === "number" && value["full_output"].bytes !== cursor["end"]) return false;
|
|
53
|
-
const origin = value["origin"];
|
|
54
|
-
return origin === undefined || object(origin) && natural(origin["node"]) && typeof origin["event"] === "string" && origin["event"] !== "";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function processReceipt(input: { session: number; source: ProcessSource; exit: number | null; stopped?: "cancelled" | "timeout";
|
|
58
|
-
output: ArtifactRef; fullOutput?: ArtifactRef; start: number; end: number; capture?: "complete" | "partial"; replayed?: boolean; artifacts?: CommandArtifactReceipt }): ProcessReceipt {
|
|
59
|
-
const state = input.exit === null ? "running" : input.stopped ?? "exited";
|
|
60
|
-
return { kind: "process", version: 1, session_id: input.session, source: input.source, state,
|
|
61
|
-
capability: input.exit === null ? "live" : "closed", running: input.exit === null, exit: input.exit,
|
|
62
|
-
output: input.output, ...(input.fullOutput ? { full_output: input.fullOutput } : {}),
|
|
63
|
-
cursor: { unit: "bytes", start: input.start, end: input.end }, replayed: input.replayed === true,
|
|
64
|
-
...(input.artifacts ? { artifacts: input.artifacts } : {}),
|
|
65
|
-
...(input.capture ? { capture: input.capture } : {}) };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Missing live state never turns a historical running observation into a fabricated exit. */
|
|
69
|
-
export function recoverProcessReceipt(receipt: ProcessReceipt, origin: { node: number; event: string }): ProcessReceipt {
|
|
70
|
-
return { ...receipt, capability: receipt.state === "running" ? "lost" : receipt.state === "unknown" ? "unknown" : "expired", replayed: true, origin };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Original results, not a bounded handoff or replayed receipt, own the observation time and evidence. */
|
|
74
|
-
export function latestProcessReceipt(nodes: readonly { node: number; events: readonly LogEvent[] }[], session: number):
|
|
75
|
-
{ receipt: ProcessReceipt; origin: { node: number; event: string } } | undefined {
|
|
76
|
-
for (let n = nodes.length - 1; n >= 0; n--) {
|
|
77
|
-
const part = nodes[n]!;
|
|
78
|
-
for (let i = part.events.length - 1; i >= 0; i--) {
|
|
79
|
-
const event = part.events[i]!, origin = { node: part.node, event: event.id };
|
|
80
|
-
if (event.t === "process_exit" && event.session === session) return { origin, receipt: event.receipt ?? {
|
|
81
|
-
kind: "process", version: 1, session_id: session, state: "exited", capability: "closed", source: null,
|
|
82
|
-
running: false, exit: event.exit, output: event.output, replayed: false,
|
|
83
|
-
} };
|
|
84
|
-
if (event.t !== "tool_result" || !object(event.data) || event.data["session_id"] !== session) continue;
|
|
85
|
-
if (isProcessReceipt(event.data)) {
|
|
86
|
-
if (!event.data.replayed) return { receipt: event.data, origin };
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const data = event.data;
|
|
90
|
-
if (typeof data["running"] !== "boolean" || data["running"] === false && !Number.isInteger(data["exit"])) continue;
|
|
91
|
-
return { origin, receipt: { kind: "process", version: 1, session_id: session, source: null,
|
|
92
|
-
state: data["running"] ? "running" : "exited", capability: data["running"] ? "live" : "closed",
|
|
93
|
-
running: data["running"], exit: data["running"] ? null : data["exit"] as number,
|
|
94
|
-
...(event.content_ref ? { output: event.content_ref } : {}), replayed: false } };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Disposable cache bounds are supplied by runtime; pruning never changes the durable receipts. */
|
|
101
|
-
export function retainProcessReceipts<T extends { endedAt: number }>(entries: readonly T[], now: number, maxAgeMs: number, maxCount: number): T[] {
|
|
102
|
-
if (!Number.isFinite(now) || !Number.isFinite(maxAgeMs) || maxAgeMs < 0 || !Number.isSafeInteger(maxCount) || maxCount < 0) throw new Error("invalid receipt retention policy");
|
|
103
|
-
return entries.filter(entry => entry.endedAt <= now && now - entry.endedAt < maxAgeMs)
|
|
104
|
-
.sort((a, b) => b.endedAt - a.endedAt).slice(0, maxCount);
|
|
105
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/** O.14: observed process facts and present capabilities are different projections. */
|
|
2
|
-
import type { ArtifactRef, LogEvent } from "./events.ts";
|
|
3
|
-
import type { BranchRecord } from "./handoff.ts";
|
|
4
|
-
import { isProcessReceipt } from "./process-receipt.ts";
|
|
5
|
-
|
|
6
|
-
export type ProcessObservation = {
|
|
7
|
-
session: number; node: number; event: string; observed: "running" | "exited";
|
|
8
|
-
command?: string; exit?: number; output?: ArtifactRef;
|
|
9
|
-
}
|
|
10
|
-
export function deriveProcesses(prev: readonly ProcessObservation[], events: readonly LogEvent[], node: number): ProcessObservation[] {
|
|
11
|
-
const state = new Map(prev.map(item => [item.session, item]));
|
|
12
|
-
const commands = new Map<string, string>();
|
|
13
|
-
for (const event of events) {
|
|
14
|
-
if (event.t === "tool_invocation" && event.name === "exec_command" && typeof event.input === "object" && event.input !== null &&
|
|
15
|
-
!Array.isArray(event.input) && "cmd" in event.input && typeof event.input["cmd"] === "string") commands.set(event.call_id, event.input["cmd"]);
|
|
16
|
-
if (event.t === "assistant") for (const call of event.blocks) {
|
|
17
|
-
if (call.type === "tool_use" && call.name === "exec_command" && typeof call.input === "object" && call.input !== null && "cmd" in call.input && typeof call.input.cmd === "string") commands.set(call.call_id, call.input.cmd);
|
|
18
|
-
}
|
|
19
|
-
if (event.t === "process_exit") state.set(event.session, { session: event.session, node, event: event.id, observed: "exited", command: event.command, exit: event.exit, output: event.output });
|
|
20
|
-
if (event.t !== "tool_result" || !event.data || typeof event.data !== "object" || Array.isArray(event.data)) continue;
|
|
21
|
-
const data = event.data as { readonly [key: string]: unknown };
|
|
22
|
-
if (isProcessReceipt(data) && data.replayed) continue;
|
|
23
|
-
if (typeof data["session_id"] !== "number" || !Number.isSafeInteger(data["session_id"]) || data["session_id"] < 1 || typeof data["running"] !== "boolean") continue;
|
|
24
|
-
if (data["running"] === false && !Number.isInteger(data["exit"])) continue;
|
|
25
|
-
const before = state.get(data["session_id"]);
|
|
26
|
-
const command = event.effect?.kind === "command" ? event.effect.cmd : commands.get(event.call_id) ?? before?.command;
|
|
27
|
-
state.set(data["session_id"], { session: data["session_id"], node, event: event.id, observed: data["running"] ? "running" : "exited",
|
|
28
|
-
...(command ? { command } : {}), ...(typeof data["exit"] === "number" ? { exit: data["exit"] } : {}), ...(event.content_ref ? { output: event.content_ref } : {}) });
|
|
29
|
-
}
|
|
30
|
-
return [...state.values()].sort((a, b) => a.session - b.session);
|
|
31
|
-
}
|
|
32
|
-
export function processStatus(item: ProcessObservation, handle: "running" | "ended_unobserved" | undefined): "running" | "ended_unobserved" | "unknown" | "exited" {
|
|
33
|
-
return item.observed === "exited" ? "exited" : handle ?? "unknown";
|
|
34
|
-
}
|
|
35
|
-
export function branchStatus(item: BranchRecord, handle: "running" | "ready" | undefined): "running" | "ready" | "unknown" | "joined" | "resolved" {
|
|
36
|
-
return item.resolution !== null ? "resolved" : item.handoff_ref ? "joined" : handle ?? "unknown";
|
|
37
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/** OSC 7501 revision 0.2: program state, never a model's claim of goal completion (Q24/D11). */
|
|
2
|
-
import type { ExecutionOutcome } from "./outcome.ts";
|
|
3
|
-
|
|
4
|
-
export type ProgramStatus = { state: "idle" | "working" | "done" | "blocked" | "error" | "clear";
|
|
5
|
-
kind?: "permission" | "question" | "auth"; msg?: string };
|
|
6
|
-
/** S.1: what a finished turn's ledger says, in one line; the status itself stays "done". */
|
|
7
|
-
const FINISHED: Readonly<Record<string, string>> = {
|
|
8
|
-
verified: "Goal verified against its contract",
|
|
9
|
-
checked: "Turn finished; changes passed a fresh check",
|
|
10
|
-
unchecked: "Turn finished; changes not checked",
|
|
11
|
-
needs_work: "Turn finished; declared checks incomplete",
|
|
12
|
-
not_applicable: "Turn finished",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export function programStatus(input: { running: boolean; waiting?: "permission" | "question" | "auth" | null;
|
|
16
|
-
outcome?: ExecutionOutcome; goal?: string }): ProgramStatus {
|
|
17
|
-
if (input.waiting) return { state: "blocked", kind: input.waiting, msg: input.waiting === "auth" ? "Sign in to continue" : input.waiting === "permission" ? "Tool approval required" : "User input required" };
|
|
18
|
-
if (input.running) return { state: "working", msg: "Agent is working" };
|
|
19
|
-
if (input.outcome?.status === "cancelled") return { state: "idle", msg: "Turn cancelled" };
|
|
20
|
-
if (input.outcome?.status === "failed") return { state: "error", msg: "Turn failed; inspect the session" };
|
|
21
|
-
if (input.outcome?.status === "stopped") return { state: "blocked", kind: "question", msg: "Turn stopped; review evidence before continuing" };
|
|
22
|
-
if (input.goal === "awaiting_human") return { state: "blocked", kind: "question", msg: "Goal awaits user review" };
|
|
23
|
-
if (input.outcome?.reason === "model_end") return { state: "done", msg: FINISHED[input.goal ?? "not_applicable"] ?? FINISHED["not_applicable"]! };
|
|
24
|
-
return { state: "idle", msg: "Ready for a request" };
|
|
25
|
-
}
|
|
26
|
-
export const PROGRAM_STATUS_QUERY = "\x1b]7501;?\x1b\\";
|
|
27
|
-
export function programStatusReply(sequence: string): boolean { return /^\x1b\]7501;\?[^\x00-\x1f\x7f]*(?:\x07|\x1b\\)$/.test(sequence); }
|
|
28
|
-
|
|
29
|
-
export function encodeProgramStatus(status: ProgramStatus, id: string): string {
|
|
30
|
-
if (id.length > 128 || id.split("/").length > 8 || !id.split("/").every(part => /^[A-Za-z0-9_.+-]{1,32}$/.test(part))) throw Error("invalid program status id");
|
|
31
|
-
let message = "", size = 0;
|
|
32
|
-
// Protocol's decoded UTF-8 cap. Remove controls before encoding; never split a code point.
|
|
33
|
-
for (const point of (status.msg ?? "").replace(/[\x00-\x1f\x7f-\x9f\p{Cf}]/gu, " ")) {
|
|
34
|
-
const bytes = new TextEncoder().encode(point); if (size + bytes.length > 2048) break;
|
|
35
|
-
message += point; size += bytes.length;
|
|
36
|
-
}
|
|
37
|
-
const encoded = btoa(Array.from(new TextEncoder().encode(message), byte => String.fromCharCode(byte)).join(""));
|
|
38
|
-
return `\x1b]7501;state=${status.state}:app=xeno:id=${id}` +
|
|
39
|
-
(status.state === "blocked" && status.kind ? `:kind=${status.kind}` : "") + (message ? `:msg=${encoded}` : "") + "\x1b\\";
|
|
40
|
-
}
|