@visual-z/xeno 0.8.4 → 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 +8 -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 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- 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 -3108
- 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/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- 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/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- 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
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/** R8: declarations select a target; only an owned runtime observation supplies its identity. */
|
|
2
|
-
import { parseCommandArtifactPlan, validArtifactPath, type CommandArtifactPlan } from "./command-artifacts.ts";
|
|
3
|
-
import type { ArtifactRef } from "./events.ts";
|
|
4
|
-
import { isObservationArtifact } from "./observation-view.ts";
|
|
5
|
-
import type { ProcessSource } from "./process-receipt.ts";
|
|
6
|
-
|
|
7
|
-
export type RuntimeTarget = { id: string; build: string; artifacts: CommandArtifactPlan; entry: string;
|
|
8
|
-
interpreter?: string; args?: string[]; workdir?: string; port?: number };
|
|
9
|
-
export type TargetBuild = { node: number; event: string; manifest: ArtifactRef };
|
|
10
|
-
export type RuntimeInstance = { id: string; session: number; pid: number; source: ProcessSource;
|
|
11
|
-
entry: { path: string; sha256: string }; program: { path: string; sha256: string; device: string; inode: string } };
|
|
12
|
-
export type RuntimeTargetReceipt = {
|
|
13
|
-
kind: "runtime_target"; version: 1; target: string; request: string; contract_event: string;
|
|
14
|
-
state: "ready" | "not_ready" | "stale" | "unknown"; reason: string; observed_at: string;
|
|
15
|
-
source: ProcessSource; build?: TargetBuild; instance?: RuntimeInstance; port?: number;
|
|
16
|
-
checks: { build: boolean; inputs: boolean; artifacts: boolean; process: boolean; image: boolean; port: boolean | null };
|
|
17
|
-
manifest?: ArtifactRef;
|
|
18
|
-
};
|
|
19
|
-
export type TargetAcceptance = { id: string; build: string; status: "not_run" | RuntimeTargetReceipt["state"];
|
|
20
|
-
observation?: string; binding?: string; session?: number; pid?: number; entry?: string; port?: number; build_event?: string; reason?: string };
|
|
21
|
-
|
|
22
|
-
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
-
const id = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(value);
|
|
24
|
-
const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
|
|
25
|
-
const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
26
|
-
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
27
|
-
const source = (value: unknown): value is ProcessSource => record(value) && ["host", "workspace", "cwd", "runtime"].every(key => text(value[key]));
|
|
28
|
-
|
|
29
|
-
export function parseRuntimeTarget(value: unknown): RuntimeTarget {
|
|
30
|
-
if (!record(value) || Object.keys(value).some(key => !["id", "build", "artifacts", "entry", "interpreter", "args", "workdir", "port"].includes(key)) ||
|
|
31
|
-
!id(value["id"]) || !id(value["build"]) || !validArtifactPath(value["entry"])) throw Error("invalid runtime target declaration");
|
|
32
|
-
const artifacts = parseCommandArtifactPlan(value["artifacts"]);
|
|
33
|
-
if (!artifacts.paths.includes(value["entry"])) throw Error("target entry must be an observed build artifact");
|
|
34
|
-
const interpreter = value["interpreter"], args = value["args"], workdir = value["workdir"], port = value["port"];
|
|
35
|
-
if (interpreter !== undefined && (typeof interpreter !== "string" || !interpreter.startsWith("/") || /[\x00-\x1f\x7f]/.test(interpreter) || interpreter.length > 4096)) throw Error("target interpreter needs an explicit absolute path");
|
|
36
|
-
// Assumed argv budget prevents declarations from becoming an unbounded prompt or exec payload.
|
|
37
|
-
if (args !== undefined && (!Array.isArray(args) || args.length > 64 || !args.every(arg => typeof arg === "string" && !arg.includes("\0")) || JSON.stringify(args).length > 8192)) throw Error("target args must be a bounded literal string array");
|
|
38
|
-
if (workdir !== undefined && workdir !== "." && !validArtifactPath(workdir)) throw Error("target workdir must stay inside the workspace");
|
|
39
|
-
if (port !== undefined && (!natural(port) || port < 1 || port > 65535)) throw Error("target port must be 1..65535 on IPv4 loopback");
|
|
40
|
-
return { id: value["id"], build: value["build"], entry: value["entry"], artifacts,
|
|
41
|
-
...(interpreter !== undefined ? { interpreter: interpreter as string } : {}), ...(args !== undefined ? { args: [...args as string[]] } : {}),
|
|
42
|
-
...(workdir !== undefined ? { workdir: workdir as string } : {}), ...(port !== undefined ? { port: port as number } : {}) };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function targetIsReady(receipt: Pick<RuntimeTargetReceipt, "build" | "instance" | "checks">): boolean {
|
|
46
|
-
return receipt.build !== undefined && receipt.instance !== undefined && receipt.checks.build && receipt.checks.inputs && receipt.checks.artifacts &&
|
|
47
|
-
receipt.checks.process && receipt.checks.image && receipt.checks.port !== false;
|
|
48
|
-
}
|
|
49
|
-
export function isRuntimeTargetReceipt(value: unknown): value is RuntimeTargetReceipt {
|
|
50
|
-
if (!record(value) || value["kind"] !== "runtime_target" || value["version"] !== 1 || !id(value["target"]) || !text(value["request"]) ||
|
|
51
|
-
!text(value["contract_event"]) || !text(value["observed_at"]) || !text(value["reason"]) || !source(value["source"]) ||
|
|
52
|
-
!["ready", "not_ready", "stale", "unknown"].includes(String(value["state"]))) return false;
|
|
53
|
-
const build = value["build"], instance = value["instance"], checks = value["checks"], port = value["port"];
|
|
54
|
-
if (build !== undefined && (!record(build) || !natural(build["node"]) || !text(build["event"]) || !isObservationArtifact(build["manifest"]))) return false;
|
|
55
|
-
if (instance !== undefined && (!record(instance) || !text(instance["id"]) || !natural(instance["session"]) || instance["session"] < 1 ||
|
|
56
|
-
!natural(instance["pid"]) || instance["pid"] < 1 || !source(instance["source"]) ||
|
|
57
|
-
!["host", "workspace", "cwd", "runtime"].every(key => (instance["source"] as Record<string, string>)[key] === (value["source"] as Record<string, string>)[key]) ||
|
|
58
|
-
!record(instance["entry"]) || !text(instance["entry"]["path"]) || !digest(instance["entry"]["sha256"]) ||
|
|
59
|
-
!record(instance["program"]) || !text(instance["program"]["path"]) || !digest(instance["program"]["sha256"]) ||
|
|
60
|
-
!text(instance["program"]["device"]) || !text(instance["program"]["inode"]))) return false;
|
|
61
|
-
if (!record(checks) || !["build", "inputs", "artifacts", "process", "image"].every(key => typeof checks[key] === "boolean") ||
|
|
62
|
-
!(checks["port"] === null || typeof checks["port"] === "boolean") ||
|
|
63
|
-
(port === undefined ? checks["port"] !== null : !natural(port) || port < 1 || port > 65535 || checks["port"] === null)) return false;
|
|
64
|
-
if (value["manifest"] !== undefined && !isObservationArtifact(value["manifest"])) return false;
|
|
65
|
-
return value["state"] !== "ready" || targetIsReady(value as RuntimeTargetReceipt) && isObservationArtifact(value["manifest"]);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** lsof -F0 is a field protocol; matching a process name or display substring proves nothing. */
|
|
69
|
-
export function lsofFiles(output: string, pid: number): Record<string, string>[] {
|
|
70
|
-
let process = -1, file: Record<string, string> | undefined;
|
|
71
|
-
const files: Record<string, string>[] = [];
|
|
72
|
-
for (const chunk of output.split("\0")) {
|
|
73
|
-
const field = chunk.replace(/^\n/, "");
|
|
74
|
-
if (!field) continue;
|
|
75
|
-
const key = field[0]!, value = field.slice(1);
|
|
76
|
-
if (key === "p") { process = Number(value); file = undefined; }
|
|
77
|
-
else if (key === "f") { file = { f: value }; if (process === pid) files.push(file); }
|
|
78
|
-
else if (file && process === pid) {
|
|
79
|
-
if (key === "T") { const split = value.indexOf("="); if (split > 0) file[`T${value.slice(0, split)}`] = value.slice(split + 1); }
|
|
80
|
-
else file[key] = value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return files;
|
|
84
|
-
}
|
|
85
|
-
export function lsofOwnsImage(output: string, pid: number, device: string, inode: string): boolean {
|
|
86
|
-
return lsofFiles(output, pid).some(file => {
|
|
87
|
-
if (file["f"] !== "txt" || file["i"] !== inode || !/^0x[0-9a-f]+$/i.test(file["D"] ?? "")) return false;
|
|
88
|
-
try { return BigInt(file["D"]!) === BigInt(device); } catch { return false; }
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
export function lsofOwnsPort(output: string, pid: number, port: number): boolean {
|
|
92
|
-
return lsofFiles(output, pid).some(file => file["t"] === "IPv4" && file["P"] === "TCP" && file["TST"] === "LISTEN" &&
|
|
93
|
-
["*", "127.0.0.1"].some(host => file["n"] === `${host}:${port}`));
|
|
94
|
-
}
|
package/src/core/scheduling.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/** O.5: shared work may overlap only before the next exclusive barrier. */
|
|
2
|
-
export function schedulingWindow(modes: readonly ("parallel" | "exclusive")[], start: number, limit: number): number {
|
|
3
|
-
if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("maxParallelTools must be a positive integer");
|
|
4
|
-
if (modes[start] !== "parallel") return Math.min(start + 1, modes.length);
|
|
5
|
-
let end = start;
|
|
6
|
-
while (end < modes.length && modes[end] === "parallel" && end - start < limit) end++;
|
|
7
|
-
return end;
|
|
8
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/** R2: scan completeness and preview completeness answer different questions. */
|
|
2
|
-
import { headTailSelection, isObservationArtifact, isBytePreview, type BytePreview } from "./observation-view.ts";
|
|
3
|
-
import type { ArtifactRef } from "./events.ts";
|
|
4
|
-
import { artifactRecovery } from "./prompt-policy.ts";
|
|
5
|
-
|
|
6
|
-
export interface SearchRow { prefix: string; text: string; }
|
|
7
|
-
export interface SearchScan {
|
|
8
|
-
status: "complete" | "partial" | "unknown";
|
|
9
|
-
reasons: { code: string; path?: string }[];
|
|
10
|
-
examined_files: number | null;
|
|
11
|
-
source_versions: "recorded" | "unknown";
|
|
12
|
-
}
|
|
13
|
-
export interface SearchObservation {
|
|
14
|
-
engine: "walk" | "ripgrep" | "fd" | "directory";
|
|
15
|
-
rows: SearchRow[];
|
|
16
|
-
scan: SearchScan;
|
|
17
|
-
sources: { path: string; sha256: string }[];
|
|
18
|
-
raw?: Uint8Array;
|
|
19
|
-
rawErrors?: Uint8Array;
|
|
20
|
-
limit: number;
|
|
21
|
-
empty: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface SearchReceipt {
|
|
25
|
-
kind: "search";
|
|
26
|
-
version: 1;
|
|
27
|
-
source: { host: string; workspace: string; resource: string };
|
|
28
|
-
engine: SearchObservation["engine"];
|
|
29
|
-
scan: { status: SearchScan["status"]; reasons: string[]; examined_files: number | null; source_versions: SearchScan["source_versions"] };
|
|
30
|
-
matches: number;
|
|
31
|
-
artifact: ArtifactRef;
|
|
32
|
-
manifest: ArtifactRef;
|
|
33
|
-
raw?: ArtifactRef;
|
|
34
|
-
raw_errors?: ArtifactRef;
|
|
35
|
-
preview: BytePreview;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
39
|
-
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
40
|
-
export function isSearchReceipt(value: unknown): value is SearchReceipt {
|
|
41
|
-
if (!object(value) || value["kind"] !== "search" || value["version"] !== 1 || !isObservationArtifact(value["artifact"]) || !isObservationArtifact(value["manifest"]) || !natural(value["matches"])) return false;
|
|
42
|
-
if (!["walk", "ripgrep", "fd", "directory"].includes(String(value["engine"])) || value["raw"] !== undefined && !isObservationArtifact(value["raw"])) return false;
|
|
43
|
-
if (value["raw_errors"] !== undefined && !isObservationArtifact(value["raw_errors"])) return false;
|
|
44
|
-
const source = value["source"], scan = value["scan"];
|
|
45
|
-
if (!object(source) || !["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "")) return false;
|
|
46
|
-
if (!object(scan) || !["complete", "partial", "unknown"].includes(String(scan["status"])) || !["recorded", "unknown"].includes(String(scan["source_versions"])) ||
|
|
47
|
-
!Array.isArray(scan["reasons"]) || !scan["reasons"].every(reason => typeof reason === "string") || scan["examined_files"] !== null && !natural(scan["examined_files"])) return false;
|
|
48
|
-
if (scan["status"] === "complete" && scan["reasons"].length !== 0 || scan["status"] !== "complete" && scan["reasons"].length === 0) return false;
|
|
49
|
-
return isBytePreview(value["preview"], value["artifact"].bytes);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const encoder = new TextEncoder();
|
|
53
|
-
const bytes = (text: string) => encoder.encode(text).length;
|
|
54
|
-
export const searchObservationText = (rows: readonly SearchRow[]): string => rows.map(row => row.prefix + row.text).join("\n");
|
|
55
|
-
|
|
56
|
-
/** Map count, per-line, and UTF-8 byte cuts back to the same immutable full observation. */
|
|
57
|
-
export function previewSearchObservation(observation: SearchObservation, artifact: string, maxBytes: number, maxLineChars = 500, notice = "", tools?: readonly string[]) {
|
|
58
|
-
let text = "", source = 0, view = 0;
|
|
59
|
-
const spans: { view: number; source: number; bytes: number }[] = [];
|
|
60
|
-
const mapped = (value: string) => { const size = bytes(value); if (size) spans.push({ view, source, bytes: size }); text += value; view += size; };
|
|
61
|
-
const synthetic = (value: string) => { text += value; view += bytes(value); };
|
|
62
|
-
if (notice) synthetic(notice + "\n");
|
|
63
|
-
const shown = Math.max(0, Math.min(observation.rows.length, observation.limit));
|
|
64
|
-
for (let index = 0; index < observation.rows.length; index++) {
|
|
65
|
-
const row = observation.rows[index]!;
|
|
66
|
-
if (index < shown) {
|
|
67
|
-
let end = Math.min(maxLineChars, row.text.length);
|
|
68
|
-
if (end > 0 && end < row.text.length && /[\uD800-\uDBFF]/.test(row.text[end - 1]!)) end--;
|
|
69
|
-
const kept = row.prefix + row.text.slice(0, end);
|
|
70
|
-
mapped(kept);
|
|
71
|
-
if (end < row.text.length) synthetic(`… [line truncated; ${artifactRecovery(artifact, source + bytes(kept), tools)}]`);
|
|
72
|
-
}
|
|
73
|
-
source += bytes(row.prefix + row.text);
|
|
74
|
-
if (index < observation.rows.length - 1) { if (index < shown) mapped("\n"); source++; }
|
|
75
|
-
}
|
|
76
|
-
if (!observation.rows.length) synthetic(observation.scan.status === "complete" ? observation.empty : "[No matches observed in the scanned portion]");
|
|
77
|
-
if (shown < observation.rows.length) synthetic(`[${observation.rows.length - shown} more matches omitted; ${artifactRecovery(artifact, 0, tools)}]`);
|
|
78
|
-
if (observation.scan.status !== "complete") synthetic(`\n[scan ${observation.scan.status}: ${[...new Set(observation.scan.reasons.map(reason => reason.code === "match_timeout" ? "search cut off: match_timeout" : reason.code))].join(", ")}]`);
|
|
79
|
-
const previewBytes = encoder.encode(text);
|
|
80
|
-
const selected = headTailSelection(previewBytes, previewBytes, previewBytes.length, maxBytes,
|
|
81
|
-
omitted => `\n[truncated ${omitted} preview bytes; ${artifactRecovery(artifact, 0, tools)}]\n`);
|
|
82
|
-
const visible: [number, number][] = [];
|
|
83
|
-
for (const [lo, hi] of selected.visible) for (const span of spans) {
|
|
84
|
-
const begin = Math.max(lo, span.view), end = Math.min(hi, span.view + span.bytes);
|
|
85
|
-
if (end <= begin) continue;
|
|
86
|
-
const a = span.source + begin - span.view, b = span.source + end - span.view;
|
|
87
|
-
const last = visible.at(-1);
|
|
88
|
-
if (last?.[1] === a) last[1] = b;
|
|
89
|
-
else visible.push([a, b]);
|
|
90
|
-
}
|
|
91
|
-
let next = 0;
|
|
92
|
-
for (const [lo, hi] of visible) { if (lo !== next) break; next = hi; }
|
|
93
|
-
const full = encoder.encode(searchObservationText(observation.rows));
|
|
94
|
-
const semantic = visible.map(([lo, hi]) => new TextDecoder().decode(full.subarray(lo, hi))).join("");
|
|
95
|
-
return { text: selected.text, semantic, preview: { unit: "utf8_bytes" as const, complete: next === source,
|
|
96
|
-
visible, next_offset: next === source ? null : next } };
|
|
97
|
-
}
|
package/src/core/secrets.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credential redaction at the log boundary (S.9).
|
|
3
|
-
*
|
|
4
|
-
* The log is the only truth and every model request is a projection of it, so
|
|
5
|
-
* a credential that reaches the log is resent on every later request and
|
|
6
|
-
* survives every cut through `objective` and `user_turns` (D2). Masking at
|
|
7
|
-
* projection time would leave the original in an append-only file forever.
|
|
8
|
-
* Redaction therefore happens before an event is written: the secret is
|
|
9
|
-
* replaced by a placeholder, and the value lives in a separate per-session
|
|
10
|
-
* vault that no projection reads.
|
|
11
|
-
*
|
|
12
|
-
* The placeholder is an exact inverse: tools expand it back to the value when
|
|
13
|
-
* they run (OpenHands' SecretRegistry does the same with secret names), and
|
|
14
|
-
* any output that echoes a known value is masked again. A false positive
|
|
15
|
-
* therefore costs readability, never correctness -- an edit whose old_string
|
|
16
|
-
* holds a placeholder still matches the file.
|
|
17
|
-
*
|
|
18
|
-
* Only high-confidence shapes are detected (gitleaks prefix rules plus
|
|
19
|
-
* "secret-named key = value"); entropy detection is deliberately absent because
|
|
20
|
-
* its false positives on hashes and code are irreversible in a live view.
|
|
21
|
-
* A bare password has no shape, so it is recognised only by context: a short
|
|
22
|
-
* token the user sends right after the assistant asked for a credential.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
export interface SecretEntry {
|
|
26
|
-
/** The placeholder's number: `[[xeno-secret-<n>]]`. */
|
|
27
|
-
n: number;
|
|
28
|
-
value: string;
|
|
29
|
-
/** Which rule found it, for the audit trail; never the value. */
|
|
30
|
-
rule: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type Vault = readonly SecretEntry[];
|
|
34
|
-
|
|
35
|
-
export const placeholder = (n: number): string => `[[xeno-secret-${n}]]`;
|
|
36
|
-
const PLACEHOLDER = /\[\[xeno-secret-(\d+)\]\]/g;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* A known value shorter than this is too likely to occur by accident ("admin",
|
|
40
|
-
* "123456") to mask everywhere it appears. Assumed, not measured; Codex's
|
|
41
|
-
* key-assignment rule uses the same 8.
|
|
42
|
-
*/
|
|
43
|
-
export const MIN_SECRET_LENGTH = 8;
|
|
44
|
-
|
|
45
|
-
interface Rule { id: string; pattern: RegExp; /** Capture group holding the secret; 0 is the whole match. */ group: number }
|
|
46
|
-
|
|
47
|
-
// Prefix rules follow gitleaks' default config (b58d3f10) and Gemini CLI's
|
|
48
|
-
// environmentSanitization.ts; tskey- is added for Tailscale, which gitleaks lacks.
|
|
49
|
-
const RULES: readonly Rule[] = [
|
|
50
|
-
{ id: "pem-private-key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g, group: 0 },
|
|
51
|
-
{ id: "anthropic-key", pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}/g, group: 0 },
|
|
52
|
-
{ id: "openai-key", pattern: /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}/g, group: 0 },
|
|
53
|
-
{ id: "github-token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{22,})/g, group: 0 },
|
|
54
|
-
{ id: "gitlab-token", pattern: /\bglpat-[A-Za-z0-9_-]{20,}/g, group: 0 },
|
|
55
|
-
{ id: "slack-token", pattern: /\bxox[abposr]-[A-Za-z0-9-]{10,}/g, group: 0 },
|
|
56
|
-
{ id: "aws-access-key", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, group: 0 },
|
|
57
|
-
{ id: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}/g, group: 0 },
|
|
58
|
-
{ id: "stripe-key", pattern: /\b[sr]k_live_[0-9A-Za-z]{24,}/g, group: 0 },
|
|
59
|
-
// tskey-<kind>-<id with a digit>-<secret>; documentation like "tskey-auth-reusable-" has no digit in the id.
|
|
60
|
-
{ id: "tailscale-key", pattern: /\btskey-[a-z]+-[A-Za-z0-9]*\d[A-Za-z0-9]*-[A-Za-z0-9]{10,}/g, group: 0 },
|
|
61
|
-
{ id: "npm-token", pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, group: 0 },
|
|
62
|
-
{ id: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, group: 0 },
|
|
63
|
-
{ id: "bearer-token", pattern: /\bBearer\s+([A-Za-z0-9._~+/=-]{20,})/g, group: 1 },
|
|
64
|
-
{ id: "url-password", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:([^\s/@]{3,})@/gi, group: 1 },
|
|
65
|
-
// A secret-named key assigned a literal. The value class excludes quotes,
|
|
66
|
-
// brackets and parentheses so `token = getToken()` and `password: string`
|
|
67
|
-
// stay code; requiring a digit or 16+ characters keeps `secret: undefined`.
|
|
68
|
-
{ id: "secret-assignment", group: 1,
|
|
69
|
-
// Up to three prefix words, so access_token, HUILIANYI_REFRESH_TOKEN and db-password count.
|
|
70
|
-
pattern: /\b(?:[a-z0-9]+[_-]){0,3}(?:api[_-]?key|access[_-]?key|secret(?:[_-]?key)?|client[_-]?secret|token|password|passwd|pwd)["']?\s*[:=]\s*["']?([^\s"'`(){}[\]<>;,]{8,})/gi },
|
|
71
|
-
{ id: "secret-assignment-zh", group: 1, pattern: /(?:密码|口令|密钥|令牌)\s*(?:是|为|[::=])\s*["'“]?([^\s"'”,。,;;]{4,})/g },
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
const plausibleAssigned = (value: string): boolean =>
|
|
75
|
-
!/^(?:\$|process\.|os\.|env\.|self\.|this\.)/.test(value) && !value.startsWith("[[xeno-secret-") &&
|
|
76
|
-
// A pure number after a token-named key is a count (input_tokens=12000000), not a credential.
|
|
77
|
-
!/^\d+$/.test(value) &&
|
|
78
|
-
// Credentials carry digits; a digit-free value is a path, a member access or a placeholder word
|
|
79
|
-
// (replayed 2026-10-12 sessions: "internal/server/…", "auth.AccessToken"). A long mixed-case
|
|
80
|
-
// value without separators is the one digit-free exception.
|
|
81
|
-
(/\d/.test(value) && /[A-Za-z]/.test(value) || value.length >= 20 && /[A-Z]/.test(value) && /[a-z]/.test(value) && !/[./]/.test(value));
|
|
82
|
-
|
|
83
|
-
/** Every secret-shaped span in `text`, with the rule that found it. Overlaps resolve to the earliest, longest. */
|
|
84
|
-
export function findSecrets(text: string): { start: number; end: number; rule: string }[] {
|
|
85
|
-
const spans: { start: number; end: number; rule: string }[] = [];
|
|
86
|
-
for (const rule of RULES) {
|
|
87
|
-
for (const match of text.matchAll(rule.pattern)) {
|
|
88
|
-
const value = match[rule.group];
|
|
89
|
-
if (value === undefined || match.index === undefined) continue;
|
|
90
|
-
if (rule.id === "secret-assignment" && !plausibleAssigned(value)) continue;
|
|
91
|
-
const start = match.index + match[0].lastIndexOf(value);
|
|
92
|
-
spans.push({ start, end: start + value.length, rule: rule.id });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
spans.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
96
|
-
const kept: typeof spans = [];
|
|
97
|
-
for (const span of spans) if (kept.length === 0 || span.start >= kept.at(-1)!.end) kept.push(span);
|
|
98
|
-
return kept;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Replaces known values and newly found secrets with placeholders. Returns the
|
|
103
|
-
* vault entries it had to add; the caller persists them before the text is
|
|
104
|
-
* written anywhere, so a placeholder in the log always has a value to expand to.
|
|
105
|
-
*/
|
|
106
|
-
export function redactText(text: string, vault: Vault, candidates: readonly { value: string; rule: string }[] = []): { text: string; added: SecretEntry[] } {
|
|
107
|
-
if (text.length === 0) return { text, added: [] };
|
|
108
|
-
const added: SecretEntry[] = [];
|
|
109
|
-
let next = vault.reduce((max, entry) => Math.max(max, entry.n), 0) + 1;
|
|
110
|
-
// Candidates (secret-named environment values) join the vault only when they
|
|
111
|
-
// actually appear, so a placeholder number is never spent on a value no event holds.
|
|
112
|
-
const known: SecretEntry[] = [...vault];
|
|
113
|
-
for (const candidate of candidates) {
|
|
114
|
-
if (candidate.value.length < MIN_SECRET_LENGTH || !text.includes(candidate.value) || known.some(entry => entry.value === candidate.value)) continue;
|
|
115
|
-
const entry = { n: next++, value: candidate.value, rule: candidate.rule };
|
|
116
|
-
known.push(entry); added.push(entry);
|
|
117
|
-
}
|
|
118
|
-
let out = maskKnown(text, known);
|
|
119
|
-
const spans = findSecrets(out);
|
|
120
|
-
if (spans.length === 0) return { text: out, added };
|
|
121
|
-
let result = "", at = 0;
|
|
122
|
-
for (const span of spans) {
|
|
123
|
-
const value = out.slice(span.start, span.end);
|
|
124
|
-
const existing = known.find(entry => entry.value === value);
|
|
125
|
-
const entry = existing ?? { n: next++, value, rule: span.rule };
|
|
126
|
-
if (!existing) { known.push(entry); added.push(entry); }
|
|
127
|
-
result += out.slice(at, span.start) + placeholder(entry.n);
|
|
128
|
-
at = span.end;
|
|
129
|
-
}
|
|
130
|
-
// Values found in this pass may occur again where a neighbouring match split
|
|
131
|
-
// them (two tokens with no delimiter); a second known-value pass closes that.
|
|
132
|
-
return { text: maskKnown(result + out.slice(at), known), added };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Longest first, so a value containing another is masked whole. A short value is masked only where a rule found it. */
|
|
136
|
-
function maskKnown(text: string, known: readonly SecretEntry[]): string {
|
|
137
|
-
let out = text;
|
|
138
|
-
for (const entry of known.filter(entry => entry.value.length >= MIN_SECRET_LENGTH).sort((a, b) => b.value.length - a.value.length)) {
|
|
139
|
-
if (out.includes(entry.value)) out = out.split(entry.value).join(placeholder(entry.n));
|
|
140
|
-
}
|
|
141
|
-
return out;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** The inverse of redactText for tool inputs. An unknown placeholder is left as written. */
|
|
145
|
-
export function expandText(text: string, vault: Vault): string {
|
|
146
|
-
if (!text.includes("[[xeno-secret-")) return text;
|
|
147
|
-
return text.replace(PLACEHOLDER, (whole, n: string) => vault.find(entry => entry.n === Number(n))?.value ?? whole);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const ASKED_FOR_CREDENTIAL = /password|passcode|passphrase|credential|api[ _-]?key|access[ _-]?token|\btoken\b|密码|口令|密钥|凭据|凭证|令牌|验证码/i;
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* A bare password has no shape; what identifies it is the exchange. A message
|
|
154
|
-
* that is a single token with mixed character classes, sent right after the
|
|
155
|
-
* assistant asked for a credential, is that credential (the 2026-10-12 ticket
|
|
156
|
-
* session: the assistant asked for a login password, the user replied with it).
|
|
157
|
-
*/
|
|
158
|
-
export function isSolicitedSecret(userText: string, previousAssistantText: string | undefined): boolean {
|
|
159
|
-
const value = userText.trim();
|
|
160
|
-
if (previousAssistantText === undefined || !ASKED_FOR_CREDENTIAL.test(previousAssistantText)) return false;
|
|
161
|
-
if (!/^\S{6,128}$/.test(value) || value.startsWith("[[xeno-secret-")) return false;
|
|
162
|
-
// Letters with digits, or three character classes: "dsm612566" and "Pa$$word"
|
|
163
|
-
// qualify, an identifier like "OPENAI_API_KEY" or a branch name does not.
|
|
164
|
-
const classes = [/[a-z]/, /[A-Z]/, /\d/, /[^A-Za-z0-9]/].filter(pattern => pattern.test(value)).length;
|
|
165
|
-
const lettersAndDigits = /[A-Za-z]/.test(value) && /\d/.test(value);
|
|
166
|
-
return (lettersAndDigits || classes >= 3) && !/^[a-z]+:\/\//i.test(value) && !/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/.test(value);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Applies `redact` to every string in a JSON value, preserving its shape. */
|
|
170
|
-
export function mapStrings(value: unknown, redact: (text: string) => string, skip: (key: string) => boolean = () => false): unknown {
|
|
171
|
-
if (typeof value === "string") return redact(value);
|
|
172
|
-
if (Array.isArray(value)) return value.map(item => mapStrings(item, redact, skip));
|
|
173
|
-
if (value !== null && typeof value === "object") {
|
|
174
|
-
const out: Record<string, unknown> = {};
|
|
175
|
-
for (const [key, item] of Object.entries(value)) out[key] = skip(key) ? item : mapStrings(item, redact, skip);
|
|
176
|
-
return out;
|
|
177
|
-
}
|
|
178
|
-
return value;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** Environment variables whose names say they hold a secret; their values are masked wherever they appear. */
|
|
182
|
-
export function secretEnvironmentValues(env: Readonly<Record<string, string | undefined>>): { value: string; rule: string }[] {
|
|
183
|
-
return Object.entries(env).flatMap(([name, value]) =>
|
|
184
|
-
value !== undefined && value.length >= MIN_SECRET_LENGTH && !value.startsWith("/") &&
|
|
185
|
-
/(?:^|_)(?:API_?KEY|KEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIALS?|AUTH)(?:_|$)/i.test(name) &&
|
|
186
|
-
!/(?:^|_)(?:PATH|DIR|FILE|URL|HOST|PORT|USER|NAME|ID|MODE|TYPE)$/i.test(name)
|
|
187
|
-
? [{ value, rule: `env:${name}` }] : []).sort((a, b) => a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0);
|
|
188
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
/** Archive paths are a closed storage vocabulary, never arbitrary filesystem destinations (D3). */
|
|
2
|
-
export function archivePath(path: unknown): path is string {
|
|
3
|
-
return typeof path === "string" && /^(?:node-\d{4,}\.jsonl|handoff-\d{4,}\.yaml|manifest\.json|metadata\.jsonl|(?:artifacts|blobs)\/[a-f0-9]{2}\/[a-f0-9]{62})$/.test(path);
|
|
4
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/** Pi session-selector-search (MIT, 02eed88), independent of its AgentSession and clock. */
|
|
2
|
-
export type SessionSort = "threaded" | "recent" | "relevance";
|
|
3
|
-
export interface SessionRow {
|
|
4
|
-
path: string; id: string; name: string | null; updated_at: string | null;
|
|
5
|
-
workspace?: string; parent_session?: string; text?: string;
|
|
6
|
-
}
|
|
7
|
-
export interface SearchQuery {
|
|
8
|
-
tokens: { kind: "fuzzy" | "phrase"; value: string }[];
|
|
9
|
-
regex?: RegExp;
|
|
10
|
-
error?: string;
|
|
11
|
-
}
|
|
12
|
-
export function parseSessionQuery(query: string): SearchQuery {
|
|
13
|
-
const trimmed = query.trim();
|
|
14
|
-
if (trimmed.startsWith("re:")) {
|
|
15
|
-
if (!trimmed.slice(3).trim()) return { tokens: [], error: "Empty regex" };
|
|
16
|
-
try { return { tokens: [], regex: new RegExp(trimmed.slice(3).trim(), "i") }; }
|
|
17
|
-
catch (error) { return { tokens: [], error: String(error) }; }
|
|
18
|
-
}
|
|
19
|
-
const tokens: SearchQuery["tokens"] = [];
|
|
20
|
-
let buffer = "", quoted = false;
|
|
21
|
-
const flush = () => { if (buffer.trim()) tokens.push({ kind: quoted ? "phrase" : "fuzzy", value: buffer.trim() }); buffer = ""; };
|
|
22
|
-
for (const char of trimmed) {
|
|
23
|
-
if (char === '"') { flush(); quoted = !quoted; }
|
|
24
|
-
else if (!quoted && /\s/.test(char)) flush();
|
|
25
|
-
else buffer += char;
|
|
26
|
-
}
|
|
27
|
-
if (quoted) return { tokens: trimmed.split(/\s+/).filter(Boolean).map(value => ({ kind: "fuzzy", value })) };
|
|
28
|
-
flush(); return { tokens };
|
|
29
|
-
}
|
|
30
|
-
export function selectSessions<T extends SessionRow>(rows: readonly T[], query: string, sort: SessionSort, named: boolean,
|
|
31
|
-
fuzzy: (query: string, text: string) => { matches: boolean; score: number }): (T & { depth: number })[] {
|
|
32
|
-
const parsed = parseSessionQuery(query);
|
|
33
|
-
if (parsed.error) return [];
|
|
34
|
-
const scored = rows.flatMap(row => {
|
|
35
|
-
if (named && !row.name?.trim()) return [];
|
|
36
|
-
const text = `${row.id} ${row.name ?? ""} ${row.text ?? ""} ${row.workspace ?? ""}`;
|
|
37
|
-
let score = 0;
|
|
38
|
-
if (parsed.regex) { const i = text.search(parsed.regex); if (i < 0) return []; score = i * 0.1; }
|
|
39
|
-
else for (const token of parsed.tokens) {
|
|
40
|
-
if (token.kind === "phrase") {
|
|
41
|
-
const i = text.toLowerCase().replace(/\s+/g, " ").indexOf(token.value.toLowerCase().replace(/\s+/g, " "));
|
|
42
|
-
if (i < 0) return []; score += i * 0.1;
|
|
43
|
-
} else { const match = fuzzy(token.value, text); if (!match.matches) return []; score += match.score; }
|
|
44
|
-
}
|
|
45
|
-
return [{ row, score }];
|
|
46
|
-
});
|
|
47
|
-
const recent = (a: SessionRow, b: SessionRow) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "") || a.path.localeCompare(b.path);
|
|
48
|
-
scored.sort((a, b) => query.trim() && sort !== "recent" ? a.score - b.score || recent(a.row, b.row) : recent(a.row, b.row));
|
|
49
|
-
if (sort !== "threaded" || query.trim()) return scored.map(({ row }) => ({ ...row, depth: 0 }));
|
|
50
|
-
const byPath = new Map(scored.map(({ row }) => [row.path, row]));
|
|
51
|
-
const children = new Map<string, T[]>();
|
|
52
|
-
const roots: T[] = [];
|
|
53
|
-
for (const { row } of scored) {
|
|
54
|
-
if (row.parent_session && row.parent_session !== row.path && byPath.has(row.parent_session)) {
|
|
55
|
-
const list = children.get(row.parent_session) ?? []; list.push(row); children.set(row.parent_session, list);
|
|
56
|
-
} else roots.push(row);
|
|
57
|
-
}
|
|
58
|
-
// Allocation can be arbitrarily deep. Iterative traversal handles cycles without hiding their rows.
|
|
59
|
-
const latest = new Map(scored.map(({ row }) => [row.path, row.updated_at ?? ""]));
|
|
60
|
-
for (const { row } of scored) {
|
|
61
|
-
const visited = new Set<string>([row.path]);
|
|
62
|
-
let parent = row.parent_session;
|
|
63
|
-
while (parent && byPath.has(parent) && !visited.has(parent)) {
|
|
64
|
-
visited.add(parent);
|
|
65
|
-
if ((row.updated_at ?? "") > (latest.get(parent) ?? "")) latest.set(parent, row.updated_at ?? "");
|
|
66
|
-
parent = byPath.get(parent)?.parent_session;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const byActivity = (a: T, b: T) => (latest.get(b.path) ?? "").localeCompare(latest.get(a.path) ?? "") || recent(a, b);
|
|
70
|
-
roots.sort(byActivity); for (const list of children.values()) list.sort(byActivity);
|
|
71
|
-
const out: (T & { depth: number })[] = [], visited = new Set<string>();
|
|
72
|
-
for (const root of [...roots, ...scored.map(s => s.row)]) {
|
|
73
|
-
const stack = [{ row: root, depth: 0 }];
|
|
74
|
-
while (stack.length) {
|
|
75
|
-
const next = stack.pop()!;
|
|
76
|
-
if (visited.has(next.row.path)) continue;
|
|
77
|
-
visited.add(next.row.path); out.push({ ...next.row, depth: next.depth });
|
|
78
|
-
for (const row of [...children.get(next.row.path) ?? []].reverse()) stack.push({ row, depth: next.depth + 1 });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/** Session intent is decided before any log is opened (D8, Pi entry parity). */
|
|
2
|
-
export interface SessionEntryFlags {
|
|
3
|
-
explicit?: string;
|
|
4
|
-
fresh?: boolean;
|
|
5
|
-
continueLatest?: boolean;
|
|
6
|
-
noSession?: boolean;
|
|
7
|
-
session?: string;
|
|
8
|
-
sessionId?: string;
|
|
9
|
-
resume?: string | boolean;
|
|
10
|
-
forkFrom?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type SessionEntry =
|
|
14
|
-
| { kind: "new" }
|
|
15
|
-
| { kind: "ephemeral" }
|
|
16
|
-
| { kind: "continue" }
|
|
17
|
-
| { kind: "select" }
|
|
18
|
-
| { kind: "named"; id: string }
|
|
19
|
-
| { kind: "open"; target: string; explicit: boolean }
|
|
20
|
-
| { kind: "fork"; target: string };
|
|
21
|
-
|
|
22
|
-
export function planSessionEntry(flags: SessionEntryFlags): SessionEntry {
|
|
23
|
-
const choices: { flag: string; entry: SessionEntry }[] = [];
|
|
24
|
-
if (flags.explicit) choices.push({ flag: "--log", entry: { kind: "open", target: flags.explicit, explicit: true } });
|
|
25
|
-
if (flags.session) choices.push({ flag: "--session", entry: { kind: "open", target: flags.session, explicit: false } });
|
|
26
|
-
if (flags.sessionId) {
|
|
27
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(flags.sessionId)) throw new Error("--session-id requires a plain session identifier");
|
|
28
|
-
choices.push({ flag: "--session-id", entry: { kind: "named", id: flags.sessionId } });
|
|
29
|
-
}
|
|
30
|
-
if (flags.resume) choices.push({ flag: "--resume", entry: typeof flags.resume === "string"
|
|
31
|
-
? { kind: "open", target: flags.resume, explicit: false } : { kind: "select" } });
|
|
32
|
-
if (flags.forkFrom) choices.push({ flag: "--fork", entry: { kind: "fork", target: flags.forkFrom } });
|
|
33
|
-
if (flags.continueLatest) choices.push({ flag: "--continue", entry: { kind: "continue" } });
|
|
34
|
-
// --fresh predates --no-session; together they still mean a fresh ephemeral log.
|
|
35
|
-
if (flags.fresh || flags.noSession) choices.push({ flag: flags.noSession ? "--no-session" : "--fresh",
|
|
36
|
-
entry: { kind: flags.noSession ? "ephemeral" : "new" } });
|
|
37
|
-
if (choices.length > 1) throw new Error(`conflicting session options: ${choices.map((c) => c.flag).join(", ")}`);
|
|
38
|
-
return choices[0]?.entry ?? { kind: "new" };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface SavedModel {
|
|
42
|
-
model: string;
|
|
43
|
-
provider?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* S.3: every cursor of this session format names its provider, so a resume
|
|
48
|
-
* restores exactly that provider/model and never guesses one from a model ID
|
|
49
|
-
* that several providers serve.
|
|
50
|
-
*/
|
|
51
|
-
export function restoreSessionModel<T extends { id: string; provider: string }>(
|
|
52
|
-
saved: SavedModel,
|
|
53
|
-
catalog: readonly T[],
|
|
54
|
-
): T {
|
|
55
|
-
if (saved.provider === undefined) throw new Error(`saved model ${saved.model} names no provider, which this session format always records. Start a new session`);
|
|
56
|
-
const matches = catalog.filter((m) => m.id === saved.model && m.provider === saved.provider);
|
|
57
|
-
const label = `${saved.provider}/${saved.model}`;
|
|
58
|
-
if (matches.length === 0) throw new Error(`saved model ${label} is unavailable. Restore its catalog entry, or start a new session`);
|
|
59
|
-
if (matches.length !== 1) throw new Error(`saved model ${label} is ambiguous across providers. Restore an unambiguous catalog before resuming`);
|
|
60
|
-
return matches[0]!;
|
|
61
|
-
}
|
package/src/core/session-exit.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/** D8 / D11: terminal exit is a bounded projection of execution facts, never a replay. */
|
|
2
|
-
import type { LogEvent } from "./events.ts";
|
|
3
|
-
import { pendingToolCalls } from "./execution.ts";
|
|
4
|
-
import { modelOutcome } from "./outcome.ts";
|
|
5
|
-
import { currentRequestEvents } from "./request-state.ts";
|
|
6
|
-
|
|
7
|
-
export const DEFAULT_FULLSCREEN_EXIT_OUTPUT = "resume-hint";
|
|
8
|
-
|
|
9
|
-
/** The identifiers `--session-id` accepts; anything else is printed as a path. */
|
|
10
|
-
const PLAIN_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The command a person retypes to come back. Claude Code prints
|
|
14
|
-
* `claude --resume <id>` and Codex `codex resume <id>`: an id, not a path.
|
|
15
|
-
* `--resume <id>` resolves only inside the sessions directory of the shell's
|
|
16
|
-
* cwd, so a session stored anywhere else (`--session-dir`, `--state-dir`,
|
|
17
|
-
* `-w` elsewhere) keeps the explicit path rather than a command that cannot
|
|
18
|
-
* find it.
|
|
19
|
-
*/
|
|
20
|
-
export function resumeCommand(session: { id: string; directory: string; path: string }, cwdSessions: string): string {
|
|
21
|
-
return session.directory === cwdSessions && PLAIN_ID.test(session.id)
|
|
22
|
-
? `xeno --resume ${session.id}` : `xeno --session ${JSON.stringify(session.path)}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Terminal controls and arbitrarily large reason text do not own the shell surface. */
|
|
26
|
-
export function exitLine(text: string, limit = 180): string {
|
|
27
|
-
return text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/\s+/g, " ").trim().slice(0, limit);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function sessionExitStatus(events: readonly LogEvent[]): string {
|
|
31
|
-
const current = currentRequestEvents(events);
|
|
32
|
-
const outcome = current.findLast(event => event.t === "outcome" || event.t === "user_bash");
|
|
33
|
-
if (outcome?.t === "outcome") return exitLine(`${outcome.outcome.status} · ${outcome.outcome.reason} · ${outcome.reason}`);
|
|
34
|
-
if (outcome?.t === "user_bash") return `${outcome.outcome.status} · ${outcome.outcome.reason} · command exit ${outcome.exit}`;
|
|
35
|
-
if (current.some(event => event.t === "interrupt")) return "cancelled · user interrupted";
|
|
36
|
-
const assistant = current.findLast(event => event.t === "assistant");
|
|
37
|
-
if (assistant?.t === "assistant" && !assistant.blocks.some(block => block.type === "tool_use") && pendingToolCalls(current).length === 0) {
|
|
38
|
-
const result = modelOutcome(assistant.stop_reason ?? null);
|
|
39
|
-
return result.reason === "model_end" ? "model finished · verification remains in session evidence" : `${result.status} · ${result.reason}`;
|
|
40
|
-
}
|
|
41
|
-
return current.length === 0 ? "session saved" : "unfinished · inspect recorded receipts before continuing";
|
|
42
|
-
}
|