@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
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { editablePath } from "./file-checkpoints.ts";
|
|
2
|
-
import { open } from "node:fs/promises";
|
|
3
|
-
import { constants } from "node:fs";
|
|
4
|
-
import { blobAddress } from "./blobs.ts";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
-
import { resolve } from "node:path";
|
|
7
|
-
|
|
8
|
-
// Operational memory ceiling: a language server is optional and must not read arbitrarily large files.
|
|
9
|
-
const MAX_LSP_DOCUMENT = 2 * 1024 * 1024;
|
|
10
|
-
export async function readLspDocument(root: string, path: string, signal?: AbortSignal): Promise<{ uri: string; text: string; sha256: string }> {
|
|
11
|
-
signal?.throwIfAborted();
|
|
12
|
-
const absolute = await editablePath(root, path);
|
|
13
|
-
const file = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
14
|
-
try {
|
|
15
|
-
const info = await file.stat();
|
|
16
|
-
if (!info.isFile() || info.nlink > 1) throw Error("LSP requires an existing ordinary workspace file");
|
|
17
|
-
if (info.size > MAX_LSP_DOCUMENT) throw Error("LSP document exceeds 2 MiB");
|
|
18
|
-
const bytes = new Uint8Array(MAX_LSP_DOCUMENT + 1); let length = 0;
|
|
19
|
-
for (;;) {
|
|
20
|
-
signal?.throwIfAborted();
|
|
21
|
-
const result = await file.read(bytes, length, bytes.length - length, null);
|
|
22
|
-
if (!result.bytesRead) break;
|
|
23
|
-
length += result.bytesRead;
|
|
24
|
-
if (length > MAX_LSP_DOCUMENT) throw Error("LSP document exceeds 2 MiB");
|
|
25
|
-
}
|
|
26
|
-
signal?.throwIfAborted();
|
|
27
|
-
const content = bytes.subarray(0, length);
|
|
28
|
-
return { uri: pathToFileURL(resolve(root, path)).href, text: new TextDecoder("utf8", { fatal: true }).decode(content), sha256: blobAddress(content) };
|
|
29
|
-
} finally { await file.close(); }
|
|
30
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/** Credential storage is separate from append-only observations: secrets must never become log evidence. */
|
|
2
|
-
import { mkdir, open, rename, rm, lstat } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
import { constants } from "node:fs";
|
|
6
|
-
import { isJsonValue, type JsonValue } from "../core/tool-contract.ts";
|
|
7
|
-
|
|
8
|
-
export interface McpCredentials {
|
|
9
|
-
version: 1; serverUrl: string; redirectUrl: string;
|
|
10
|
-
clientIdentity?: string;
|
|
11
|
-
clients: Record<string, JsonValue>; tokens: Record<string, JsonValue>; expiresAt: Record<string, number>; activeIssuer: string | null;
|
|
12
|
-
}
|
|
13
|
-
export interface McpCredentialsStore {
|
|
14
|
-
read(serverUrl: string, clientIdentity?: string): Promise<McpCredentials | undefined>;
|
|
15
|
-
write(credentials: McpCredentials): Promise<void>;
|
|
16
|
-
remove(serverUrl: string, clientIdentity?: string): Promise<void>;
|
|
17
|
-
}
|
|
18
|
-
// Assumed bound prevents a replaced credential file from becoming an unbounded JSON allocation.
|
|
19
|
-
const MAX_CREDENTIAL_BYTES = 1024 * 1024;
|
|
20
|
-
function filename(dir: string, url: string, clientIdentity = "") { return join(dir, createHash("sha256").update(JSON.stringify([url, clientIdentity])).digest("hex") + ".json"); }
|
|
21
|
-
function record(value: unknown): value is Record<string, JsonValue> {
|
|
22
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && isJsonValue(value);
|
|
23
|
-
}
|
|
24
|
-
function validate(value: unknown, serverUrl: string, clientIdentity = ""): McpCredentials {
|
|
25
|
-
if (!record(value) || value["version"] !== 1 || value["serverUrl"] !== serverUrl || typeof value["redirectUrl"] !== "string"
|
|
26
|
-
|| (value["clientIdentity"] ?? "") !== clientIdentity
|
|
27
|
-
|| !record(value["clients"]) || !record(value["tokens"]) || !record(value["expiresAt"])
|
|
28
|
-
|| Object.values(value["expiresAt"]).some(time => typeof time !== "number" || !Number.isFinite(time))
|
|
29
|
-
|| value["activeIssuer"] !== null && typeof value["activeIssuer"] !== "string") throw Error("MCP credential record is invalid");
|
|
30
|
-
return { version: 1, serverUrl, redirectUrl: value["redirectUrl"], clients: value["clients"], tokens: value["tokens"],
|
|
31
|
-
expiresAt: Object.fromEntries(Object.entries(value["expiresAt"]).map(([issuer, time]) => [issuer, Number(time)])), activeIssuer: value["activeIssuer"],
|
|
32
|
-
...(typeof value["clientIdentity"] === "string" ? { clientIdentity: value["clientIdentity"] } : {}) };
|
|
33
|
-
}
|
|
34
|
-
/** Explicit restricted-file backend, compatible with Codex's fallback; applications may provide an OS vault adapter. */
|
|
35
|
-
export class FileMcpCredentialsStore implements McpCredentialsStore {
|
|
36
|
-
private readonly dir: string;
|
|
37
|
-
private writes = Promise.resolve();
|
|
38
|
-
constructor(dir: string) { this.dir = dir; }
|
|
39
|
-
private async directory(): Promise<void> {
|
|
40
|
-
const stat = await lstat(this.dir);
|
|
41
|
-
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) throw Error("MCP credential directory permissions are invalid");
|
|
42
|
-
}
|
|
43
|
-
async read(serverUrl: string, clientIdentity?: string): Promise<McpCredentials | undefined> {
|
|
44
|
-
const path = filename(this.dir, serverUrl, clientIdentity);
|
|
45
|
-
try {
|
|
46
|
-
await this.directory();
|
|
47
|
-
const stat = await lstat(path);
|
|
48
|
-
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.size > MAX_CREDENTIAL_BYTES) throw Error("MCP credential file permissions or size are invalid");
|
|
49
|
-
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
50
|
-
try {
|
|
51
|
-
const opened = await file.stat();
|
|
52
|
-
if (!opened.isFile() || opened.size > MAX_CREDENTIAL_BYTES || (opened.mode & 0o077) !== 0) throw Error("Unsafe credential handle");
|
|
53
|
-
return validate(JSON.parse(await file.readFile("utf8")), serverUrl, clientIdentity);
|
|
54
|
-
} finally { await file.close(); }
|
|
55
|
-
} catch (error) {
|
|
56
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return undefined;
|
|
57
|
-
throw Error("MCP credential store could not be read safely");
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
write(credentials: McpCredentials): Promise<void> {
|
|
61
|
-
const serialized = JSON.stringify(validate(credentials, credentials.serverUrl, credentials.clientIdentity));
|
|
62
|
-
const target = filename(this.dir, credentials.serverUrl, credentials.clientIdentity);
|
|
63
|
-
if (Buffer.byteLength(serialized) > MAX_CREDENTIAL_BYTES) return Promise.reject(Error("MCP credential record exceeds size limit"));
|
|
64
|
-
const operation = this.writes.then(async () => {
|
|
65
|
-
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
66
|
-
await this.directory();
|
|
67
|
-
const temporary = join(this.dir, `.${randomUUID()}.tmp`);
|
|
68
|
-
try {
|
|
69
|
-
const file = await open(temporary, "wx", 0o600);
|
|
70
|
-
try { await file.writeFile(serialized); await file.sync(); } finally { await file.close(); }
|
|
71
|
-
await rename(temporary, target);
|
|
72
|
-
} finally { await rm(temporary, { force: true }); }
|
|
73
|
-
});
|
|
74
|
-
this.writes = operation.catch(() => {});
|
|
75
|
-
return operation;
|
|
76
|
-
}
|
|
77
|
-
async remove(serverUrl: string, clientIdentity?: string): Promise<void> {
|
|
78
|
-
await this.writes;
|
|
79
|
-
try { await this.directory(); await rm(filename(this.dir, serverUrl, clientIdentity), { force: true }); }
|
|
80
|
-
catch (error) {
|
|
81
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return;
|
|
82
|
-
throw Error("MCP credential store could not be removed safely");
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
package/src/store/models.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The on-disk catalog overlay. `~/.xeno/models.json`, next to auth.json.
|
|
3
|
-
*
|
|
4
|
-
* This layer only reads and writes bytes. It does not know what a ModelSpec
|
|
5
|
-
* is — that lives in runtime — because store may not import runtime. A
|
|
6
|
-
* corrupt file is returned as text plus no error: the parser names the bad
|
|
7
|
-
* entry, and discarding the whole file here would be the thing the card
|
|
8
|
-
* forbids.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { mkdir, readFile, rename, writeFile, rm } from "node:fs/promises";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import { configDir } from "./config.ts";
|
|
15
|
-
|
|
16
|
-
export function modelsPath(): string {
|
|
17
|
-
return join(configDir(), "models.json");
|
|
18
|
-
}
|
|
19
|
-
export function catalogCachePath(): string { return join(configDir(), "models-cache.json"); }
|
|
20
|
-
|
|
21
|
-
export async function readModelsText(): Promise<{ text: string | null; error: string | null }> {
|
|
22
|
-
return readModelFile(modelsPath());
|
|
23
|
-
}
|
|
24
|
-
export async function readCatalogCacheText(): Promise<{ text: string | null; error: string | null }> {
|
|
25
|
-
return readModelFile(catalogCachePath());
|
|
26
|
-
}
|
|
27
|
-
async function readModelFile(path: string): Promise<{ text: string | null; error: string | null }> {
|
|
28
|
-
try {
|
|
29
|
-
const text = await readFile(path, "utf8");
|
|
30
|
-
return { text, error: null };
|
|
31
|
-
} catch (err) {
|
|
32
|
-
const absent = typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
33
|
-
return { text: null, error: absent ? null : `cannot read ${path}` };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export async function readModelSources(): Promise<{ userText: string | null; cacheText: string | null; readWarnings: string[] }> {
|
|
37
|
-
const [user, cache] = await Promise.all([readModelsText(), readCatalogCacheText()]);
|
|
38
|
-
return { userText: user.text, cacheText: cache.text, readWarnings: [user.error, cache.error].filter((value): value is string => value !== null) };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Replace, don't truncate. writeFile on the live path empties it first, so a
|
|
43
|
-
* crash or a thrown write would leave the user with no catalog. Sidecar then
|
|
44
|
-
* rename is atomic on the same filesystem; a failed refresh must leave the
|
|
45
|
-
* existing bytes untouched (feat-json-mode-and-header).
|
|
46
|
-
*/
|
|
47
|
-
export async function writeModelsFile(value: unknown): Promise<void> {
|
|
48
|
-
return writeModelFile(modelsPath(), value);
|
|
49
|
-
}
|
|
50
|
-
export async function writeCatalogCache(value: unknown): Promise<void> {
|
|
51
|
-
return writeModelFile(catalogCachePath(), value);
|
|
52
|
-
}
|
|
53
|
-
async function writeModelFile(dest: string, value: unknown): Promise<void> {
|
|
54
|
-
await mkdir(configDir(), { recursive: true });
|
|
55
|
-
const tmp = `${dest}.${randomUUID()}.tmp`;
|
|
56
|
-
try {
|
|
57
|
-
await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
58
|
-
await rename(tmp, dest);
|
|
59
|
-
} finally { await rm(tmp, { force: true }); }
|
|
60
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/** Store complete observations while keeping only a bounded preview in memory. */
|
|
2
|
-
import { createReadStream } from "node:fs";
|
|
3
|
-
import { open, mkdir, rename, rm } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import type { ArtifactRef } from "../core/events.ts";
|
|
7
|
-
import { blobFilePath, verifyBlob } from "./blobs.ts";
|
|
8
|
-
|
|
9
|
-
/** Byte pages keep very large single-line observations retrievable as well. */
|
|
10
|
-
export async function readObservation(dir: string, sha256: string, offset: number, limit: number): Promise<{
|
|
11
|
-
sha256: string; bytes: number; offset: number; next: number | null; text: string; base64: string;
|
|
12
|
-
}> {
|
|
13
|
-
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1) throw new Error("invalid observation page");
|
|
14
|
-
if (!await verifyBlob(dir, sha256)) throw new Error(`missing observation ${sha256}`);
|
|
15
|
-
const file = await open(blobFilePath(dir, sha256), "r");
|
|
16
|
-
try {
|
|
17
|
-
const bytes = (await file.stat()).size;
|
|
18
|
-
const buffer = Buffer.alloc(Math.min(limit, Math.max(0, bytes - offset)));
|
|
19
|
-
const { bytesRead } = await file.read(buffer, 0, buffer.length, offset);
|
|
20
|
-
const data = buffer.subarray(0, bytesRead);
|
|
21
|
-
return { sha256, bytes, offset, next: offset + bytesRead < bytes ? offset + bytesRead : null,
|
|
22
|
-
text: data.toString("utf8"), base64: data.toString("base64") };
|
|
23
|
-
} finally { await file.close(); }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* After the shell exits, how long silence must last before capture stops.
|
|
28
|
-
*
|
|
29
|
-
* A background child (`server &`) inherits the pipes, so waiting for EOF
|
|
30
|
-
* waits for the server -- the call then blocks until the timeout and the
|
|
31
|
-
* timeout kills the server with the group. Operational choice, not measured:
|
|
32
|
-
* long enough for buffered output of the exited shell to arrive, short
|
|
33
|
-
* enough that a background start costs nothing noticeable. pi uses the same
|
|
34
|
-
* idle-grace shape (`utils/child-process.ts`).
|
|
35
|
-
*/
|
|
36
|
-
const EXIT_IDLE_MS = 100;
|
|
37
|
-
/** Hard bound after exit for a background child that never goes quiet. */
|
|
38
|
-
const EXIT_DRAIN_MS = 1_000;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Both streams into one file in arrival order.
|
|
42
|
-
*
|
|
43
|
-
* A build that prints progress on stdout and the error on stderr only makes
|
|
44
|
-
* sense interleaved; appending all of stderr after all of stdout (the previous
|
|
45
|
-
* shape) separates each error from the step that produced it. Writes are
|
|
46
|
-
* serialized on one handle, so interleaving happens at chunk granularity --
|
|
47
|
-
* the same granularity a terminal shows.
|
|
48
|
-
*
|
|
49
|
-
* Capture ends at EOF on both streams, or once `exited` has resolved and the
|
|
50
|
-
* streams have gone idle. Ending early cancels the readers only; it signals
|
|
51
|
-
* no process, so a detached background child keeps running.
|
|
52
|
-
*/
|
|
53
|
-
async function spoolMerged(
|
|
54
|
-
dir: string, streams: readonly ReadableStream<Uint8Array>[], exited: Promise<number>,
|
|
55
|
-
onOutput?: (chunk: Uint8Array) => void,
|
|
56
|
-
separate = false,
|
|
57
|
-
limitBytes?: number,
|
|
58
|
-
): Promise<{ path: string; channels: string[]; complete: boolean }> {
|
|
59
|
-
await mkdir(dir, { recursive: true });
|
|
60
|
-
const path = join(dir, `.${randomUUID()}.stream`);
|
|
61
|
-
const file = await open(path, "wx", 0o600);
|
|
62
|
-
const channels: string[] = [];
|
|
63
|
-
const channelFiles: Awaited<ReturnType<typeof open>>[] = [];
|
|
64
|
-
try {
|
|
65
|
-
if (separate) for (let i = 0; i < streams.length; i++) {
|
|
66
|
-
const channel = `${path}.${i}`;
|
|
67
|
-
channelFiles.push(await open(channel, "wx", 0o600)); channels.push(channel);
|
|
68
|
-
}
|
|
69
|
-
} catch (error) {
|
|
70
|
-
await Promise.allSettled([file.close(), ...channelFiles.map(item => item.close())]);
|
|
71
|
-
await Promise.all([path, ...channels].map(item => rm(item, { force: true })));
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
const readers = streams.map((stream) => stream.getReader());
|
|
75
|
-
let queue: Promise<unknown> = Promise.resolve();
|
|
76
|
-
let exitedAt: number | undefined;
|
|
77
|
-
let idle: ReturnType<typeof setTimeout> | undefined;
|
|
78
|
-
let drain: ReturnType<typeof setTimeout> | undefined;
|
|
79
|
-
let finished = false, complete = true, capturedBytes = 0;
|
|
80
|
-
const stop = () => { if (finished) return; complete = false; for (const reader of readers) reader.cancel().catch(() => {}); };
|
|
81
|
-
const armIdle = () => {
|
|
82
|
-
if (finished || exitedAt === undefined) return;
|
|
83
|
-
clearTimeout(idle);
|
|
84
|
-
idle = setTimeout(stop, EXIT_IDLE_MS);
|
|
85
|
-
};
|
|
86
|
-
void exited.then(() => { if (finished) return; exitedAt = performance.now(); armIdle(); drain = setTimeout(stop, EXIT_DRAIN_MS); }, () => {});
|
|
87
|
-
try {
|
|
88
|
-
const drained = await Promise.allSettled(readers.map(async (reader, index) => {
|
|
89
|
-
try {
|
|
90
|
-
for (;;) {
|
|
91
|
-
const result = await reader.read();
|
|
92
|
-
if (result.done) return;
|
|
93
|
-
armIdle();
|
|
94
|
-
queue = queue.then(async () => {
|
|
95
|
-
await file.writeFile(result.value);
|
|
96
|
-
await channelFiles[index]?.writeFile(result.value);
|
|
97
|
-
onOutput?.(result.value);
|
|
98
|
-
capturedBytes += result.value.byteLength;
|
|
99
|
-
// Retain every delivered chunk, then cancel both readers. At most
|
|
100
|
-
// the in-flight crossing chunks exceed the bound; SIGTERM grace
|
|
101
|
-
// cannot turn an output limit into unlimited spool growth (R6).
|
|
102
|
-
if (limitBytes !== undefined && capturedBytes > limitBytes) stop();
|
|
103
|
-
});
|
|
104
|
-
await queue;
|
|
105
|
-
}
|
|
106
|
-
} catch (error) { stop(); throw error; }
|
|
107
|
-
}));
|
|
108
|
-
await queue;
|
|
109
|
-
for (const result of drained) if (result.status === "rejected") throw result.reason;
|
|
110
|
-
} catch (error) {
|
|
111
|
-
await Promise.allSettled([file.close(), ...channelFiles.map(item => item.close())]);
|
|
112
|
-
await Promise.all([path, ...channels].map(item => rm(item, { force: true })));
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
finished = true;
|
|
117
|
-
clearTimeout(idle); clearTimeout(drain);
|
|
118
|
-
for (const reader of readers) reader.releaseLock();
|
|
119
|
-
}
|
|
120
|
-
await Promise.all([file.close(), ...channelFiles.map(item => item.close())]);
|
|
121
|
-
return { path, channels, complete };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Last `limit` bytes of a stream, in O(limit) memory however long it runs. */
|
|
125
|
-
class TailWindow {
|
|
126
|
-
private readonly parts: Uint8Array[] = [];
|
|
127
|
-
private size = 0;
|
|
128
|
-
private readonly limit: number;
|
|
129
|
-
constructor(limit: number) { this.limit = limit; }
|
|
130
|
-
push(chunk: Uint8Array): void {
|
|
131
|
-
if (this.limit === 0) return;
|
|
132
|
-
const part = chunk.byteLength > this.limit ? chunk.slice(chunk.byteLength - this.limit) : chunk.slice();
|
|
133
|
-
this.parts.push(part);
|
|
134
|
-
this.size += part.byteLength;
|
|
135
|
-
while (this.size - this.parts[0]!.byteLength >= this.limit) this.size -= this.parts.shift()!.byteLength;
|
|
136
|
-
}
|
|
137
|
-
bytes(): Uint8Array {
|
|
138
|
-
const all = Buffer.concat(this.parts);
|
|
139
|
-
return all.subarray(Math.max(0, all.byteLength - this.limit));
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export interface ArchivedObservation {
|
|
144
|
-
ref: ArtifactRef;
|
|
145
|
-
/** First `previewBytes` bytes, decoded; kept for callers that only page from the start. */
|
|
146
|
-
preview: string;
|
|
147
|
-
/** First and last `previewBytes` bytes, for a head-and-tail view. */
|
|
148
|
-
head: Uint8Array;
|
|
149
|
-
tail: Uint8Array;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export async function archiveObservation(
|
|
153
|
-
dir: string, source: AsyncIterable<Uint8Array>, previewBytes: number,
|
|
154
|
-
): Promise<ArchivedObservation> {
|
|
155
|
-
await mkdir(dir, { recursive: true });
|
|
156
|
-
const path = join(dir, `.${randomUUID()}.observation`);
|
|
157
|
-
const file = await open(path, "wx", 0o600);
|
|
158
|
-
const hash = createHash("sha256");
|
|
159
|
-
let bytes = 0;
|
|
160
|
-
let kept = 0;
|
|
161
|
-
const preview: Uint8Array[] = [];
|
|
162
|
-
const tail = new TailWindow(previewBytes);
|
|
163
|
-
try {
|
|
164
|
-
try {
|
|
165
|
-
for await (const chunk of source) {
|
|
166
|
-
hash.update(chunk);
|
|
167
|
-
bytes += chunk.byteLength;
|
|
168
|
-
if (kept < previewBytes) {
|
|
169
|
-
const part = chunk.slice(0, previewBytes - kept);
|
|
170
|
-
preview.push(part); kept += part.byteLength;
|
|
171
|
-
}
|
|
172
|
-
tail.push(chunk);
|
|
173
|
-
await file.writeFile(chunk);
|
|
174
|
-
}
|
|
175
|
-
await file.sync();
|
|
176
|
-
} finally { await file.close(); }
|
|
177
|
-
const sha256 = hash.digest("hex");
|
|
178
|
-
const destination = blobFilePath(dir, sha256);
|
|
179
|
-
await mkdir(join(dir, sha256.slice(0, 2)), { recursive: true });
|
|
180
|
-
await rename(path, destination);
|
|
181
|
-
const shard = await open(join(dir, sha256.slice(0, 2)), "r");
|
|
182
|
-
try { await shard.sync(); } finally { await shard.close(); }
|
|
183
|
-
const head = Buffer.concat(preview);
|
|
184
|
-
return {
|
|
185
|
-
ref: { sha256, path: destination, bytes },
|
|
186
|
-
preview: new TextDecoder().decode(head, { stream: kept < bytes }),
|
|
187
|
-
head, tail: tail.bytes(),
|
|
188
|
-
};
|
|
189
|
-
} finally { await rm(path, { force: true }); }
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* The artifact is `exit N\n` followed by the merged output. Nothing that varies
|
|
194
|
-
* run to run (wall time) belongs here: the artifact digest is the D9 novelty
|
|
195
|
-
* key, and a timestamp in it would make every rerun look like new evidence.
|
|
196
|
-
*/
|
|
197
|
-
export async function captureCommandObservation(
|
|
198
|
-
dir: string, stdout: ReadableStream<Uint8Array>, stderr: ReadableStream<Uint8Array>,
|
|
199
|
-
exited: Promise<number>, previewBytes: number,
|
|
200
|
-
onOutput?: (chunk: Uint8Array) => void,
|
|
201
|
-
separate = false,
|
|
202
|
-
limitBytes?: number,
|
|
203
|
-
): Promise<ArchivedObservation & { exit: number; prefixBytes: number; channels: ArtifactRef[]; capture: "complete" | "partial" }> {
|
|
204
|
-
const merged = await spoolMerged(dir, [stdout, stderr], exited, onOutput, separate, limitBytes);
|
|
205
|
-
try {
|
|
206
|
-
const exit = await exited;
|
|
207
|
-
const prefix = new TextEncoder().encode(`exit ${exit}\n`);
|
|
208
|
-
async function* combined(): AsyncGenerator<Uint8Array> {
|
|
209
|
-
yield prefix;
|
|
210
|
-
for await (const chunk of createReadStream(merged.path)) yield chunk as Uint8Array;
|
|
211
|
-
}
|
|
212
|
-
async function* channel(path: string): AsyncGenerator<Uint8Array> {
|
|
213
|
-
for await (const chunk of createReadStream(path)) yield chunk as Uint8Array;
|
|
214
|
-
}
|
|
215
|
-
// Drain all archive attempts before removing their source files, including failures.
|
|
216
|
-
const attempts = await Promise.allSettled([archiveObservation(dir, combined(), previewBytes),
|
|
217
|
-
...merged.channels.map(path => archiveObservation(dir, channel(path), 0))]);
|
|
218
|
-
const stored = attempts.map(result => { if (result.status === "rejected") throw result.reason; return result.value; });
|
|
219
|
-
return { ...stored[0]!, exit, prefixBytes: prefix.byteLength, channels: stored.slice(1).map(item => item.ref),
|
|
220
|
-
capture: merged.complete ? "complete" : "partial" };
|
|
221
|
-
} finally {
|
|
222
|
-
await Promise.all([merged.path, ...merged.channels].map(path => rm(path, { force: true })));
|
|
223
|
-
}
|
|
224
|
-
}
|
package/src/store/packages.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/** Package storage is local metadata; only runtime/package-manager may install sources. */
|
|
2
|
-
import { mkdir, stat } from "node:fs/promises";
|
|
3
|
-
import { join, resolve, relative, isAbsolute, sep } from "node:path";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
import { readJsonFile } from "./config.ts";
|
|
7
|
-
import { atomicWrite } from "./atomic.ts";
|
|
8
|
-
import { serialized } from "./serial.ts";
|
|
9
|
-
import { packageConfig, packageDeclaration, parsePackageSource, type PackageDeclaration, type PackageSource } from "../core/packages.ts";
|
|
10
|
-
import type { ResourceKind } from "../core/resource-selection.ts";
|
|
11
|
-
|
|
12
|
-
export type PackageScope = "user" | "project" | "cli";
|
|
13
|
-
export interface PackageRecord { source: string; identity: string; path: string; version?: string; revision?: string }
|
|
14
|
-
export interface PackageRoot {
|
|
15
|
-
source: PackageSource; declaration: Exclude<PackageDeclaration,string>; scope: PackageScope; base: string;
|
|
16
|
-
root?: string; delta?: Exclude<PackageDeclaration,string>; trusted: boolean;
|
|
17
|
-
}
|
|
18
|
-
export function packageSource(source: string, base: string): PackageSource {
|
|
19
|
-
return parsePackageSource(source, path => resolve(base, path.startsWith("~/") ? join(homedir(),path.slice(2)) : path === "~" ? homedir() : path));
|
|
20
|
-
}
|
|
21
|
-
export function packageKey(source: PackageSource): string {
|
|
22
|
-
// Config identity omits versions, but installed references must retain them:
|
|
23
|
-
// publishing a new pin cannot invalidate the old declaration before its atomic write.
|
|
24
|
-
const version=source.kind==="npm"?source.version:source.kind==="git"?source.ref:undefined;
|
|
25
|
-
return createHash("sha256").update(source.id+"\0"+(version??"")).digest("hex");
|
|
26
|
-
}
|
|
27
|
-
export function packageHome(base: string): string { return join(base,"packages"); }
|
|
28
|
-
export function managedPackagePath(base: string, path: string): string {
|
|
29
|
-
const home = packageHome(base), target = resolve(home,path), rel = relative(home,target);
|
|
30
|
-
if (!rel || rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) throw Error("Package path escapes its managed directory");
|
|
31
|
-
return target;
|
|
32
|
-
}
|
|
33
|
-
export async function packageRecords(base: string): Promise<Record<string, PackageRecord>> {
|
|
34
|
-
const read = await readJsonFile<Record<string,PackageRecord>>(join(packageHome(base),"installed.json"));
|
|
35
|
-
if (read.error) throw Error(read.error);
|
|
36
|
-
if (read.value && (typeof read.value !== "object" || Array.isArray(read.value))) throw Error("Invalid package installation index");
|
|
37
|
-
const records=read.value ?? {};
|
|
38
|
-
for(const record of Object.values(records)) {
|
|
39
|
-
if (!record || typeof record.source !== "string" || typeof record.identity !== "string" || typeof record.path !== "string") throw Error("Invalid package installation entry");
|
|
40
|
-
managedPackagePath(base,record.path);
|
|
41
|
-
}
|
|
42
|
-
return records;
|
|
43
|
-
}
|
|
44
|
-
export async function savePackageRecord(base: string, source: PackageSource, record: PackageRecord | undefined): Promise<void> {
|
|
45
|
-
const path=join(packageHome(base),"installed.json");
|
|
46
|
-
await serialized(path,async()=>{const records=await packageRecords(base); if(record) records[packageKey(source)]=record; else delete records[packageKey(source)]; await mkdir(packageHome(base),{recursive:true}); await atomicWrite(path,JSON.stringify(records,null,2)+"\n");});
|
|
47
|
-
}
|
|
48
|
-
export async function removePackageRecords(base:string,identity:string):Promise<string[]> {
|
|
49
|
-
const path=join(packageHome(base),"installed.json");
|
|
50
|
-
return serialized(path,async()=>{const records=await packageRecords(base),keys=Object.keys(records).filter(key=>records[key]!.identity===identity);for(const key of keys)delete records[key];await mkdir(packageHome(base),{recursive:true});await atomicWrite(path,JSON.stringify(records,null,2)+"\n");return keys;});
|
|
51
|
-
}
|
|
52
|
-
export async function installedPackage(source: PackageSource, base: string): Promise<string | undefined> {
|
|
53
|
-
if(source.kind==="local") return await stat(source.path).then(()=>source.path,()=>undefined);
|
|
54
|
-
const record=(await packageRecords(base))[packageKey(source)];
|
|
55
|
-
if (!record || record.identity!==source.id) return undefined;
|
|
56
|
-
const installed=packageSource(record.source,base);
|
|
57
|
-
if(source.kind==="npm"&&(installed.kind!=="npm"||installed.version!==source.version)||source.kind==="git"&&(installed.kind!=="git"||installed.ref!==source.ref))return undefined;
|
|
58
|
-
const path=managedPackagePath(base,record.path); return await stat(path).then(s=>s.isDirectory()?path:undefined,()=>undefined);
|
|
59
|
-
}
|
|
60
|
-
export async function configuredPackages(base: string): Promise<PackageDeclaration[]> {
|
|
61
|
-
const read=await readJsonFile<Record<string,unknown>>(join(base,"settings.json")); if(read.error)throw Error(read.error);
|
|
62
|
-
if(read.value!==null&&(typeof read.value!=="object"||Array.isArray(read.value)))throw Error("Settings must be an object");
|
|
63
|
-
const list=read.value?.["packages"] ?? []; if(!Array.isArray(list)||!list.every(packageDeclaration))throw Error("Invalid packages configuration"); return list;
|
|
64
|
-
}
|
|
65
|
-
export async function savePackageDeclaration(base: string, source: PackageSource, declaration: PackageDeclaration | undefined): Promise<void> {
|
|
66
|
-
const file=join(base,"settings.json");
|
|
67
|
-
await serialized(file,async()=>{
|
|
68
|
-
const read=await readJsonFile<Record<string,unknown>>(file); if(read.error)throw Error(read.error);
|
|
69
|
-
if(read.value!==null&&(typeof read.value!=="object"||Array.isArray(read.value)))throw Error("Settings must be an object");
|
|
70
|
-
const previous=read.value?.["packages"]??[]; if(!Array.isArray(previous)||!previous.every(packageDeclaration))throw Error("Invalid packages configuration");
|
|
71
|
-
const packages=previous.filter(entry=>packageSource(packageConfig(entry).source,base).id!==source.id);
|
|
72
|
-
if(declaration!==undefined)packages.push(declaration);
|
|
73
|
-
await mkdir(base,{recursive:true}); await atomicWrite(file,JSON.stringify({...read.value,packages},null,2)+"\n");
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
export async function packageRoots(user: readonly PackageDeclaration[], project: readonly PackageDeclaration[], opts:{userDir:string;projectDir:string;trusted:boolean}, diagnostics:string[]):Promise<PackageRoot[]> {
|
|
77
|
-
const roots=new Map<string,PackageRoot>();
|
|
78
|
-
for(const scope of ["user","project"] as const) for(const entry of scope==="user"?user:project) {
|
|
79
|
-
try {
|
|
80
|
-
const declaration=packageConfig(entry),base=scope==="user"?opts.userDir:opts.projectDir,source=packageSource(declaration.source,base),existing=roots.get(source.id);
|
|
81
|
-
if(scope==="project"&&existing&&(!opts.trusted||declaration.autoload===false)) { if(opts.trusted)existing.delta=declaration; continue; }
|
|
82
|
-
const root=await installedPackage(source,base);
|
|
83
|
-
if(!root)diagnostics.push(`Package ${source.source} is not installed; run xeno install ${source.source}${scope==="project"?" --local":""}`);
|
|
84
|
-
roots.set(source.id,{source,declaration,scope,base,...(root?{root}:{}),trusted:scope==="user"||opts.trusted});
|
|
85
|
-
} catch(error){diagnostics.push(String(error));}
|
|
86
|
-
}
|
|
87
|
-
return [...roots.values()];
|
|
88
|
-
}
|
|
89
|
-
export async function packageManifest(root: string): Promise<Partial<Record<ResourceKind, readonly string[]>>> {
|
|
90
|
-
if(await stat(root).then(s=>s.isFile(),()=>false)) return {extensions:[root],skills:[],prompts:[],themes:[]};
|
|
91
|
-
const read=await readJsonFile<{xeno?:unknown;pi?:unknown}>(join(root,"package.json"));if(read.error)throw Error(read.error);
|
|
92
|
-
const value=read.value?.xeno??read.value?.pi;
|
|
93
|
-
if(value===undefined)return {extensions:["extensions"],skills:["skills"],prompts:["prompts"],themes:["themes"]};
|
|
94
|
-
if(!value||typeof value!=="object"||Array.isArray(value))throw Error("Invalid package resource manifest");
|
|
95
|
-
const result:Partial<Record<ResourceKind,readonly string[]>>={};
|
|
96
|
-
for(const kind of ["extensions","skills","prompts","themes"] as const){const paths=(value as Record<string,unknown>)[kind]??[];if(!Array.isArray(paths)||paths.some(path=>typeof path!=="string"))throw Error("Invalid package manifest " + kind);result[kind]=paths;}
|
|
97
|
-
return result;
|
|
98
|
-
}
|
package/src/store/patch.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/** R4 / P.3: one fully prepared plan shares the existing confined checkpoint CAS. */
|
|
2
|
-
import { relative, resolve } from "node:path";
|
|
3
|
-
import { parsePatch, applyPatchChunks, patchPaths, PatchError, type PatchOperation } from "../core/patch.ts";
|
|
4
|
-
import { sameVersion, changedPaths, type FileChanges } from "../core/checkpoints.ts";
|
|
5
|
-
import { unifiedFileDiff } from "../core/review.ts";
|
|
6
|
-
import { isFileDigest } from "../core/file-precondition.ts";
|
|
7
|
-
import { ToolFault, NOT_STARTED, READ_ATTEMPTED } from "../core/tool-failure.ts";
|
|
8
|
-
import { editablePath, readEditableFile, applyFileChanges } from "./file-checkpoints.ts";
|
|
9
|
-
import { putBlob } from "./blobs.ts";
|
|
10
|
-
|
|
11
|
-
export interface PreparedPatch {
|
|
12
|
-
root: string;
|
|
13
|
-
changes: FileChanges;
|
|
14
|
-
operations: readonly PatchOperation[];
|
|
15
|
-
diffs: readonly { path: string; destination?: string; kind: "add" | "update" | "move" | "delete"; diff: string }[];
|
|
16
|
-
}
|
|
17
|
-
export interface PreparePatchOptions {
|
|
18
|
-
observedFileVersion?: (absolute: string) => Promise<string | undefined>;
|
|
19
|
-
expectedSha256?: Readonly<Record<string, string>>;
|
|
20
|
-
signal?: AbortSignal;
|
|
21
|
-
}
|
|
22
|
-
const decode = (bytes: Uint8Array) => {
|
|
23
|
-
try { return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
24
|
-
catch { throw new ToolFault("invalid_input", "patch requires valid UTF-8 files; use a trusted command for binary data", READ_ATTEMPTED); }
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/** No workspace write occurs until every source, destination and hunk passes. */
|
|
28
|
-
export async function preparePatch(root: string, blobs: string, text: string, options: PreparePatchOptions = {}): Promise<PreparedPatch> {
|
|
29
|
-
let operations: PatchOperation[];
|
|
30
|
-
try { operations = parsePatch(text); }
|
|
31
|
-
catch (error) { if (error instanceof PatchError) throw new ToolFault(error.code, error.message, NOT_STARTED); throw error; }
|
|
32
|
-
const names = new Map<string, string>();
|
|
33
|
-
const targets = new Set<string>();
|
|
34
|
-
for (const path of patchPaths(operations)) {
|
|
35
|
-
const absolute = await editablePath(root, path), canonical = relative(resolve(root), absolute);
|
|
36
|
-
if (targets.has(canonical) || [...targets].some(existing => existing.startsWith(canonical + "/") || canonical.startsWith(existing + "/")))
|
|
37
|
-
throw new ToolFault("invalid_input", `patch has duplicate or overlapping targets: ${path}`, NOT_STARTED);
|
|
38
|
-
names.set(path, canonical); targets.add(canonical);
|
|
39
|
-
}
|
|
40
|
-
const canonical = (path: string) => names.get(path)!;
|
|
41
|
-
const observed = new Map<string, Awaited<ReturnType<typeof readEditableFile>>>();
|
|
42
|
-
for (const path of targets) {
|
|
43
|
-
if (options.signal?.aborted) throw new ToolFault("cancelled", "patch preparation cancelled", NOT_STARTED);
|
|
44
|
-
observed.set(path, await readEditableFile(root, path));
|
|
45
|
-
}
|
|
46
|
-
const changes: FileChanges = Object.create(null) as FileChanges;
|
|
47
|
-
const diffs: PreparedPatch["diffs"][number][] = [];
|
|
48
|
-
for (const operation of operations) {
|
|
49
|
-
const path = canonical(operation.path), source = observed.get(path)!;
|
|
50
|
-
if (operation.kind !== "add") {
|
|
51
|
-
if (source.bytes === null || source.version === null) throw new ToolFault("not_found", `patch source is missing: ${path}`, READ_ATTEMPTED);
|
|
52
|
-
const explicit = options.expectedSha256?.[operation.path] ?? options.expectedSha256?.[path];
|
|
53
|
-
const expected = explicit ?? await options.observedFileVersion?.(resolve(root, path));
|
|
54
|
-
if (!isFileDigest(expected) || expected !== source.version.sha256)
|
|
55
|
-
throw new ToolFault("version_mismatch", `patch source version was not observed or changed: ${path}; read it before editing`, READ_ATTEMPTED, { recover: { tool: "read", input: { path } } });
|
|
56
|
-
} else if (source.version !== null) {
|
|
57
|
-
throw new ToolFault("version_mismatch", `patch add requires an absent destination: ${path}; existing bytes were preserved`, READ_ATTEMPTED);
|
|
58
|
-
}
|
|
59
|
-
let destination = path, content: string | null;
|
|
60
|
-
const before = source.bytes === null ? null : decode(source.bytes);
|
|
61
|
-
if (operation.kind === "delete") content = null;
|
|
62
|
-
else if (operation.kind === "add") content = operation.content;
|
|
63
|
-
else {
|
|
64
|
-
try { content = applyPatchChunks(before!, operation.chunks); }
|
|
65
|
-
catch (error) { if (error instanceof PatchError) throw new ToolFault(error.code, `${path}: ${error.message}`, READ_ATTEMPTED); throw error; }
|
|
66
|
-
if (operation.movePath !== undefined) {
|
|
67
|
-
destination = canonical(operation.movePath);
|
|
68
|
-
if (observed.get(destination)!.version !== null) throw new ToolFault("version_mismatch", `patch move requires an absent destination: ${destination}`, READ_ATTEMPTED);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (source.bytes !== null) await putBlob(blobs, source.bytes);
|
|
72
|
-
const after = content === null ? null : { sha256: await putBlob(blobs, content), mode: source.version?.mode ?? (0o666 & ~process.umask()) };
|
|
73
|
-
if (destination !== path) {
|
|
74
|
-
changes[path] = { before: source.version, after: null };
|
|
75
|
-
changes[destination] = { before: null, after };
|
|
76
|
-
} else changes[path] = { before: source.version, after };
|
|
77
|
-
diffs.push({ path, ...(destination === path ? {} : { destination }), kind: destination !== path ? "move" : operation.kind,
|
|
78
|
-
diff: unifiedFileDiff(path, before, content, destination) });
|
|
79
|
-
}
|
|
80
|
-
return { root: resolve(root), operations, changes, diffs };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** The caller persists prepared/applied checkpoint phases around this operation. */
|
|
84
|
-
export async function applyPreparedPatch(root: string, blobs: string, prepared: PreparedPatch, signal?: AbortSignal): Promise<void> {
|
|
85
|
-
if (resolve(root) !== prepared.root) throw new ToolFault("invalid_input", "prepared patch names another workspace", NOT_STARTED);
|
|
86
|
-
if (signal?.aborted) throw new ToolFault("cancelled", "patch application cancelled", NOT_STARTED);
|
|
87
|
-
await applyFileChanges(root, blobs, prepared.changes, signal);
|
|
88
|
-
for (const path of changedPaths(prepared.changes)) {
|
|
89
|
-
if (!sameVersion((await readEditableFile(root, path)).version, prepared.changes[path]!.after))
|
|
90
|
-
throw new Error(`file changed concurrently after patch: ${path}; inspect the prepared checkpoint`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** R13 / D13: path policy sees aliases and actual resources; confinement remains separate. */
|
|
2
|
-
import { realpath } from "node:fs/promises";
|
|
3
|
-
import { dirname, basename, resolve, relative, isAbsolute } from "node:path";
|
|
4
|
-
import { permissionRequest, normalizePermissionPath, type PermissionRequest } from "../core/permissions.ts";
|
|
5
|
-
|
|
6
|
-
async function canonicalPath(path: string): Promise<string> {
|
|
7
|
-
try { return await realpath(path); }
|
|
8
|
-
catch (error) {
|
|
9
|
-
if (!error || typeof error !== "object" || !("code" in error) || !["ENOENT", "ENOTDIR"].includes(String(error.code))) throw error;
|
|
10
|
-
const parent = dirname(path);
|
|
11
|
-
if (parent === path) throw error;
|
|
12
|
-
// New files have no realpath yet; existing ancestors still reveal aliases.
|
|
13
|
-
return resolve(await canonicalPath(parent), basename(path));
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function resource(root: string, absolute: string): string {
|
|
17
|
-
const path = relative(root, absolute);
|
|
18
|
-
return normalizePermissionPath(path === ".." || path.startsWith("../") || isAbsolute(path) ? absolute : path || ".");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function permissionPaths(root: string, paths: readonly string[]): Promise<string[]> {
|
|
22
|
-
const absoluteRoot = resolve(root), canonicalRoot = await canonicalPath(absoluteRoot), resources: string[] = [];
|
|
23
|
-
for (const path of paths) {
|
|
24
|
-
const absolute = resolve(absoluteRoot, path);
|
|
25
|
-
resources.push(resource(absoluteRoot, absolute), resource(canonicalRoot, await canonicalPath(absolute)));
|
|
26
|
-
}
|
|
27
|
-
return [...new Set(resources)];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Models never supply the root or canonical target; the current runtime does. */
|
|
31
|
-
export async function canonicalPermissionRequest(root: string, name: string, input: unknown): Promise<PermissionRequest> {
|
|
32
|
-
const request = permissionRequest(name, input);
|
|
33
|
-
return request.paths ? { ...request, paths: await permissionPaths(root, request.paths) } : request;
|
|
34
|
-
}
|