@piagent/platform 1.37.0 → 1.38.1
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 +70 -0
- package/README.md +5 -3
- package/README.vi.md +2 -2
- package/catalog/capabilities.json +2 -2
- package/docs/command-reference-vietnamese.md +4 -3
- package/docs/distribution-standard.md +6 -6
- package/docs/en/maintainer-guide.md +8 -0
- package/docs/en/workspace-editor.md +222 -0
- package/docs/languages.json +13 -4
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +3 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/research/ide-m0-spike.md +101 -0
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/maintainer-guide.md +8 -0
- package/docs/vi/workspace-editor.md +217 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/index.html +4 -4
- package/docs-site/content/en/whats-new.html +23 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +23 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +5 -5
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +24 -1
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +5 -5
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +24 -1
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/piagent-webui/checkpoint-v1.invalid.json +21 -0
- package/evals/fixtures/piagent-webui/checkpoint-v1.valid.json +20 -0
- package/evals/fixtures/piagent-webui/terminal-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/terminal-v1.valid.json +16 -0
- package/evals/fixtures/piagent-webui/workspace-file-v1.invalid.json +24 -0
- package/evals/fixtures/piagent-webui/workspace-file-v1.valid.json +23 -0
- package/evals/fixtures/piagent-webui/workspace-paths-v1.invalid.json +24 -0
- package/evals/fixtures/piagent-webui/workspace-paths-v1.valid.json +24 -0
- package/evals/fixtures/piagent-webui/workspace-search-v1.invalid.json +26 -0
- package/evals/fixtures/piagent-webui/workspace-search-v1.valid.json +26 -0
- package/evals/fixtures/piagent-webui/workspace-tree-v1.invalid.json +28 -0
- package/evals/fixtures/piagent-webui/workspace-tree-v1.valid.json +27 -0
- package/evals/fixtures/piagent-webui/workspace-write-v1.invalid.json +19 -0
- package/evals/fixtures/piagent-webui/workspace-write-v1.valid.json +19 -0
- package/evals/long-task-soak-v1/README.md +17 -0
- package/evals/long-task-soak-v1/lib/session-loop.mjs +19 -3
- package/evals/long-task-soak-v1/scenarios/ide-editor-terminal.json +19 -0
- package/evals/long-task-soak-v1/surfaces/webui-gateway.mjs +17 -2
- package/evals/long-task-soak-v1/surfaces/workspace-actions.mjs +95 -0
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +7 -3
- package/packages/piagent-core/extensions/fallback-base-policy.ts +1 -1
- package/packages/piagent-core/extensions/guard-types.ts +3 -1
- package/packages/piagent-core/extensions/path-suggestions.ts +137 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +85 -7
- package/packages/piagent-core/extensions/workspace-inspection.ts +145 -0
- package/packages/piagent-core/extensions/workspace-policy.d.ts +7 -0
- package/packages/piagent-core/extensions/workspace-policy.js +3 -0
- package/packages/piagent-core/extensions/workspace-search-index.mjs +8 -0
- package/packages/piagent-core/extensions/workspace-search.ts +74 -0
- package/packages/piagent-core/extensions/workspace-write.ts +116 -0
- package/packages/piagent-core/managed/background-process.mjs +9 -4
- package/packages/piagent-core/managed/boundary-package-proxy.mjs +21 -0
- package/packages/piagent-core/managed/boundary-queue.mjs +24 -0
- package/packages/piagent-core/managed/checkpoint-git.mjs +16 -0
- package/packages/piagent-core/managed/checkpoints.mjs +17 -64
- package/packages/piagent-core/managed/command-tools.mjs +6 -6
- package/packages/piagent-core/managed/continuation.mjs +3 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/backend-api/profile.json +88 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/be-readonly-fe/profile.json +130 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/data/profile.json +88 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/devops/profile.json +90 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/docs/profile.json +74 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/fullstack/profile.json +92 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/generic/profile.json +95 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/mobile/profile.json +86 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/node-typescript/profile.json +74 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/python/profile.json +75 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/web-frontend/profile.json +99 -0
- package/packages/piagent-core/managed/dist/workspace/assets/local-state-path-BrmghxEZ.js +284 -0
- package/packages/piagent-core/managed/dist/workspace/assets/packages/piagent-core/policies/base-policy.json +351 -0
- package/packages/piagent-core/managed/dist/workspace/manifest.json +65 -0
- package/packages/piagent-core/managed/dist/workspace/worker.mjs +7374 -0
- package/packages/piagent-core/managed/dist/workspace/workspace-search-index.mjs +1007 -0
- package/packages/piagent-core/managed/helper-run.mjs +11 -1
- package/packages/piagent-core/managed/helper-settlement.mjs +17 -6
- package/packages/piagent-core/managed/ip-policy.mjs +50 -0
- package/packages/piagent-core/managed/memory-lessons.mjs +4 -1
- package/packages/piagent-core/managed/memory.mjs +1 -1
- package/packages/piagent-core/managed/package-proxy.mjs +12 -14
- package/packages/piagent-core/managed/patch-snapshot.mjs +6 -8
- package/packages/piagent-core/managed/permission.mjs +29 -6
- package/packages/piagent-core/managed/repository-operation.mjs +2 -1
- package/packages/piagent-core/managed/review-snapshot-request.mjs +21 -0
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +2 -0
- package/packages/piagent-core/managed/session-mutation-observations.mjs +37 -0
- package/packages/piagent-core/managed/session.mjs +6 -7
- package/packages/piagent-core/managed/skill-feedback.mjs +9 -1
- package/packages/piagent-core/managed/source-revert-requests.mjs +21 -0
- package/packages/piagent-core/managed/studio-link.mjs +16 -0
- package/packages/piagent-core/managed/terminal-boundary.mjs +88 -0
- package/packages/piagent-core/managed/terminal-channel.mjs +66 -0
- package/packages/piagent-core/managed/terminal-runtime.mjs +64 -0
- package/packages/piagent-core/managed/terminal-seatbelt.mjs +48 -0
- package/packages/piagent-core/managed/terminal-worker.mjs +70 -0
- package/packages/piagent-core/managed/tool-boundary.mjs +45 -35
- package/packages/piagent-core/managed/tool-freshness.mjs +47 -0
- package/packages/piagent-core/managed/tool-worker.mjs +18 -3
- package/packages/piagent-core/managed/web-fetch.mjs +3 -14
- package/packages/piagent-core/managed/workspace-worker-pins.mjs +26 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +2 -1
- package/packages/piagent-core/runtime/checkpoints/checkpoint-authority.ts +43 -0
- package/packages/piagent-core/runtime/checkpoints/checkpoint-refs.mjs +51 -0
- package/packages/piagent-core/runtime/checkpoints/checkpoint-store.mjs +179 -0
- package/packages/piagent-core/runtime/checkpoints/checkpoint-worker.ts +28 -0
- package/packages/piagent-core/runtime/checkpoints/git-adapter.mjs +36 -0
- package/packages/piagent-core/runtime/checkpoints/persistent-history.mjs +96 -0
- package/packages/piagent-core/runtime/checkpoints/personal-git.ts +272 -0
- package/packages/piagent-core/runtime/checkpoints/personal-runtime.ts +158 -0
- package/packages/piagent-core/runtime/checkpoints/prune.mjs +29 -0
- package/packages/piagent-core/runtime/checkpoints/restore-preview.mjs +46 -0
- package/packages/piagent-core/runtime/checkpoints/turn-checkpoints.mjs +167 -0
- package/packages/piagent-core/runtime/hooks/session-hooks.ts +19 -16
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +3 -0
- package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +2 -2
- package/packages/piagent-core/runtime/inspection/diff-projection.ts +2 -1
- package/packages/piagent-core/runtime/inspection/managed-source-revert.ts +90 -0
- package/packages/piagent-core/runtime/inspection/review-state-projection.ts +19 -8
- package/packages/piagent-core/runtime/inspection/session-diff-projection.ts +47 -0
- package/packages/piagent-core/runtime/inspection/session-hunk-provenance.ts +56 -0
- package/packages/piagent-core/runtime/inspection/session-mutation-observer.ts +125 -0
- package/packages/piagent-core/runtime/inspection/source-action-contract.ts +123 -0
- package/packages/piagent-core/runtime/inspection/source-change-projection.ts +3 -9
- package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +3 -41
- package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +6 -36
- package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +36 -30
- package/packages/piagent-core/runtime/inspection/text-diff.ts +5 -4
- package/packages/piagent-core/runtime/inspection/workspace-worker.ts +79 -0
- package/packages/piagent-core/runtime/policy/source-index-transaction.ts +3 -10
- package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +27 -2
- package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +42 -12
- package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +10 -5
- package/packages/piagent-core/runtime/policy/workspace-write-authority.ts +51 -0
- package/packages/piagent-core/runtime/process-owner.mjs +20 -0
- package/packages/piagent-core/runtime/process-tree-watch.mjs +46 -0
- package/packages/piagent-core/runtime/quality/model-mutation-proof.ts +8 -5
- package/packages/piagent-core/runtime/registration/permission-commands.ts +10 -2
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
- package/packages/piagent-core/runtime/session/workspace-member-edits.d.mts +7 -0
- package/packages/piagent-core/runtime/session/workspace-member-edits.mjs +41 -0
- package/packages/piagent-core/security/workspace-file-reader.ts +2 -1
- package/packages/piagent-webui/dist/client/assets/Alert-DXF8-XqE.js +1 -0
- package/packages/piagent-webui/dist/client/assets/Button-DZJosXQi.js +1 -0
- package/packages/piagent-webui/dist/client/assets/DialogTitle-CL-fzCnT.js +3 -0
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-k4tq1y8s.js +29 -0
- package/packages/piagent-webui/dist/client/assets/TerminalPanel-DvOPgrGI.js +2 -0
- package/packages/piagent-webui/dist/client/assets/TerminalView-B_a16moC.js +9 -0
- package/packages/piagent-webui/dist/client/assets/TerminalView-kHJ-D0s7.css +1 -0
- package/packages/piagent-webui/dist/client/assets/Typography-DMZL1kLS.js +1 -0
- package/packages/piagent-webui/dist/client/assets/WorkspaceCodeEditor-BCiYM9NA.js +2 -0
- package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-DttpQtpf.js +5 -0
- package/packages/piagent-webui/dist/client/assets/WorkspaceQuickOpen-fgXwQ46B.js +1 -0
- package/packages/piagent-webui/dist/client/assets/bootstrap-CedkNtRG.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-B1M7IjfA.js +10 -0
- package/packages/piagent-webui/dist/client/assets/dist-BUYtGaUP.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-C82AtDDT.js +6 -0
- package/packages/piagent-webui/dist/client/assets/dist-D9b8j6pJ.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-DLfpWwLs.js +23 -0
- package/packages/piagent-webui/dist/client/assets/dist-DcEu_WYV.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-_JsfplOG.js +9 -0
- package/packages/piagent-webui/dist/client/assets/dist-idN8f9Fc.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-nvgpa2Us.js +1 -0
- package/packages/piagent-webui/dist/client/assets/index-BW8-BUMx.js +102 -0
- package/packages/piagent-webui/dist/client/assets/index-Brs_HjIo.css +1 -0
- package/packages/piagent-webui/dist/client/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/packages/piagent-webui/dist/client/assets/tab-close-HE0XxoJf.js +1 -0
- package/packages/piagent-webui/dist/client/assets/ui-preferences-DiddStu0.js +124 -0
- package/packages/piagent-webui/dist/client/assets/useTheme-_ESOj7MI.js +10 -0
- package/packages/piagent-webui/dist/client/assets/workspace-api-O7auv_td.js +1 -0
- package/packages/piagent-webui/dist/client/index.html +10 -2
- package/packages/piagent-webui/extension/same-session-bridge.ts +2 -3
- package/packages/piagent-webui/extension/source-revert-controller.ts +14 -208
- package/packages/piagent-webui/gateway/checkpoint-sessions.ts +99 -0
- package/packages/piagent-webui/gateway/checkpoint-store.ts +6 -0
- package/packages/piagent-webui/gateway/company-relay.ts +5 -0
- package/packages/piagent-webui/gateway/company-terminal-cache.ts +54 -0
- package/packages/piagent-webui/gateway/company-terminal-channel.ts +117 -0
- package/packages/piagent-webui/gateway/company-terminal-relay.ts +75 -0
- package/packages/piagent-webui/gateway/gateway-protocol-service.ts +12 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +48 -4
- package/packages/piagent-webui/gateway/managed-gateway.mjs +30 -3
- package/packages/piagent-webui/gateway/managed-projection.ts +5 -3
- package/packages/piagent-webui/gateway/managed-source-revert-controller.ts +45 -0
- package/packages/piagent-webui/gateway/managed-source-revert-sessions.ts +74 -0
- package/packages/piagent-webui/gateway/managed-terminal-binding.ts +30 -0
- package/packages/piagent-webui/gateway/managed-terminal-processes.ts +74 -0
- package/packages/piagent-webui/gateway/managed-terminal-sessions.ts +9 -0
- package/packages/piagent-webui/gateway/path-suggestions.ts +1 -124
- package/packages/piagent-webui/gateway/personal-terminal-binding.ts +36 -0
- package/packages/piagent-webui/gateway/personal-terminal-diagnostic.mjs +16 -0
- package/packages/piagent-webui/gateway/personal-terminal-processes.ts +119 -0
- package/packages/piagent-webui/gateway/personal-terminal-sessions.ts +22 -0
- package/packages/piagent-webui/gateway/runtime-session-controls.ts +2 -1
- package/packages/piagent-webui/gateway/session-catalog.ts +3 -2
- package/packages/piagent-webui/gateway/session-command-controller.ts +4 -4
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +29 -5
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -8
- package/packages/piagent-webui/gateway/signed-session-journal.ts +77 -0
- package/packages/piagent-webui/gateway/source-revert-sessions.ts +74 -0
- package/packages/piagent-webui/gateway/source-revert-store.ts +47 -0
- package/packages/piagent-webui/gateway/terminal-command-ledger.ts +33 -0
- package/packages/piagent-webui/gateway/terminal-mentions.ts +29 -0
- package/packages/piagent-webui/gateway/terminal-output-buffer.ts +58 -0
- package/packages/piagent-webui/gateway/terminal-router.ts +12 -0
- package/packages/piagent-webui/gateway/terminal-sessions.ts +247 -0
- package/packages/piagent-webui/gateway/workspace-write-sessions.ts +69 -0
- package/packages/piagent-webui/gateway/workspace-write-store.ts +6 -0
- package/packages/piagent-webui/package.json +14 -2
- package/packages/piagent-webui/server/checkpoint-command.ts +30 -0
- package/packages/piagent-webui/server/checkpoint-controller.ts +87 -0
- package/packages/piagent-webui/server/checkpoint-routes.ts +38 -0
- package/packages/piagent-webui/server/control-command-digest.ts +5 -0
- package/packages/piagent-webui/server/core-inspection-provider.ts +16 -22
- package/packages/piagent-webui/server/gateway-websocket.ts +36 -8
- package/packages/piagent-webui/server/loopback-server.ts +9 -0
- package/packages/piagent-webui/server/read-model-provider.ts +3 -0
- package/packages/piagent-webui/server/read-only-router.ts +3 -0
- package/packages/piagent-webui/server/session-auth.ts +4 -1
- package/packages/piagent-webui/server/session-source-revert-routes.ts +54 -0
- package/packages/piagent-webui/server/source-projection-binding.ts +18 -0
- package/packages/piagent-webui/server/source-revert-controller.ts +225 -0
- package/packages/piagent-webui/server/terminal-command.ts +35 -0
- package/packages/piagent-webui/server/terminal-websocket-stream.ts +56 -0
- package/packages/piagent-webui/server/transcript-projection.ts +2 -1
- package/packages/piagent-webui/server/workspace-read-routes.ts +32 -0
- package/packages/piagent-webui/server/workspace-reader.ts +23 -0
- package/packages/piagent-webui/server/workspace-write-controller.ts +118 -0
- package/packages/piagent-webui/server/workspace-write-routes.ts +38 -0
- package/packages/piagent-webui/shared/terminal-message.ts +23 -0
- package/schemas/piagent-webui/catalog-v1.json +63 -0
- package/schemas/piagent-webui/checkpoint-v1.schema.json +466 -0
- package/schemas/piagent-webui/control-command-v1.schema.json +68 -3
- package/schemas/piagent-webui/diff-v1.schema.json +4 -0
- package/schemas/piagent-webui/gateway-protocol-v1.schema.json +123 -3
- package/schemas/piagent-webui/session-catalog-v1.schema.json +1 -1
- package/schemas/piagent-webui/session-command-v1.schema.json +1 -1
- package/schemas/piagent-webui/snapshot-v1.schema.json +2 -1
- package/schemas/piagent-webui/source-revert-v1.schema.json +22 -3
- package/schemas/piagent-webui/terminal-v1.schema.json +495 -0
- package/schemas/piagent-webui/workspace-file-v1.schema.json +79 -0
- package/schemas/piagent-webui/workspace-paths-v1.schema.json +71 -0
- package/schemas/piagent-webui/workspace-search-v1.schema.json +80 -0
- package/schemas/piagent-webui/workspace-tree-v1.schema.json +87 -0
- package/schemas/piagent-webui/workspace-write-v1.schema.json +276 -0
- package/schemas/project-profile.schema.json +8 -3
- package/scripts/company-terminal.mjs +16 -11
- package/scripts/pi-auto.sh +5 -1
- package/scripts/team-doctor.sh +8 -0
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-CfgyQQbW.js +0 -29
- package/packages/piagent-webui/dist/client/assets/index-HyzWKvDu.js +0 -234
- package/packages/piagent-webui/dist/client/assets/index-_0nGP2n3.css +0 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
import { stopProcessGroup } from './background-process.mjs';
|
|
3
|
+
|
|
4
|
+
// Native effects are acknowledged once. A lost response rejects as uncertain;
|
|
5
|
+
// the Gateway command ledger owns retries and must never redispatch the effect.
|
|
6
|
+
export function terminalChannel(child, emit) {
|
|
7
|
+
const requests = new Map(), decoder = new StringDecoder('utf8');
|
|
8
|
+
let pending = '', next = 0, ready = false, closed = false, closing, exited = false;
|
|
9
|
+
let resolveReady, rejectReady, resolveClosed;
|
|
10
|
+
const initialized = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
|
|
11
|
+
const completed = new Promise(resolve => { resolveClosed = resolve; });
|
|
12
|
+
const timer = setTimeout(() => fail('managed-terminal-start-timeout'), 10000);
|
|
13
|
+
const notify = message => { try { emit(message); } catch { /* consumer never owns process lifetime */ } };
|
|
14
|
+
const rejectAll = reason => {
|
|
15
|
+
rejectReady(new Error(reason));
|
|
16
|
+
for (const request of requests.values()) { clearTimeout(request.timer); request.reject(new Error(reason)); }
|
|
17
|
+
requests.clear();
|
|
18
|
+
};
|
|
19
|
+
const close = () => closing ??= (async () => {
|
|
20
|
+
if (closed) return;
|
|
21
|
+
rejectAll('managed-terminal-effect-uncertain');
|
|
22
|
+
await stopProcessGroup(child);
|
|
23
|
+
let timeout;
|
|
24
|
+
try { await Promise.race([completed, new Promise((_, reject) => { timeout = setTimeout(() => reject(Error('managed-terminal-cleanup-unconfirmed')), 3000); })]); }
|
|
25
|
+
finally { clearTimeout(timeout); }
|
|
26
|
+
})();
|
|
27
|
+
const fail = reason => { rejectAll(reason); void close().catch(() => {}); };
|
|
28
|
+
child.once('error', () => fail('managed-terminal-sandbox-unavailable'));
|
|
29
|
+
child.once('close', () => {
|
|
30
|
+
closed = true; clearTimeout(timer); rejectAll('managed-terminal-effect-uncertain');
|
|
31
|
+
if (!exited) notify({ kind: 'exit', exitCode: null, signal: null });
|
|
32
|
+
resolveClosed();
|
|
33
|
+
});
|
|
34
|
+
child.stdin.on('error', () => fail('managed-terminal-effect-uncertain'));
|
|
35
|
+
child.stderr.resume(); // Sandbox startup errors are reported by code, not raw host paths.
|
|
36
|
+
child.stdout.on('data', chunk => {
|
|
37
|
+
pending += decoder.write(chunk);
|
|
38
|
+
if (Buffer.byteLength(pending) > 128 * 1024) return fail('managed-terminal-protocol-invalid');
|
|
39
|
+
for (;;) {
|
|
40
|
+
const end = pending.indexOf('\n'); if (end < 0) break;
|
|
41
|
+
const line = pending.slice(0, end); pending = pending.slice(end + 1);
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.parse(line), keys = Object.keys(value).sort().join(',');
|
|
44
|
+
if (value.kind === 'ready' && keys === 'kind' && !ready) { ready = true; clearTimeout(timer); resolveReady(); }
|
|
45
|
+
else if (value.kind === 'unavailable' && !ready) fail('managed-terminal-sandbox-unavailable');
|
|
46
|
+
else if (ready && value.kind === 'ack' && keys === 'id,kind' && requests.has(value.id)) {
|
|
47
|
+
const request = requests.get(value.id); clearTimeout(request.timer); requests.delete(value.id); request.resolve();
|
|
48
|
+
} else if (ready && value.kind === 'output' && keys === 'data,kind' && typeof value.data === 'string' && value.data.length <= 8192) notify(value);
|
|
49
|
+
else if (ready && value.kind === 'exit' && keys === 'exitCode,kind,signal' && (value.exitCode === null || Number.isInteger(value.exitCode))
|
|
50
|
+
&& (value.signal === null || Number.isInteger(value.signal)) && !exited) { exited = true; notify(value); }
|
|
51
|
+
else throw Error();
|
|
52
|
+
} catch { return fail('managed-terminal-protocol-invalid'); }
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return { initialized, completed, close, execute(payload) {
|
|
56
|
+
if (!ready || closed || closing || exited) return Promise.reject(Error('managed-terminal-exited'));
|
|
57
|
+
if (requests.size >= 128 || child.stdin.writableLength > 128 * 1024) return Promise.reject(Error('managed-terminal-capacity'));
|
|
58
|
+
const id = String(++next), line = JSON.stringify({ ...payload, id }) + '\n';
|
|
59
|
+
if (Buffer.byteLength(line) > 128 * 1024) return Promise.reject(Error('managed-terminal-input-limit'));
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const timer = setTimeout(() => { requests.delete(id); reject(Error('managed-terminal-effect-uncertain')); void close().catch(() => {}); }, 10000);
|
|
62
|
+
requests.set(id, { resolve, reject, timer });
|
|
63
|
+
child.stdin.write(line, error => { if (error) fail('managed-terminal-effect-uncertain'); });
|
|
64
|
+
});
|
|
65
|
+
} };
|
|
66
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { BWRAP } from './linux-sandbox.mjs';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const VERSION = '1.2.0-beta.15';
|
|
9
|
+
const fail = () => { throw new Error('managed-terminal-runtime-invalid'); };
|
|
10
|
+
|
|
11
|
+
// Inspect code without loading the native addon. The entire platform package
|
|
12
|
+
// is pinned, including its JS and prebuilt binary. Linux loads it inside the
|
|
13
|
+
// sandbox; macOS loads it on the company Gateway to make the PTY there (the
|
|
14
|
+
// shell itself runs under Seatbelt, terminal-boundary.mjs).
|
|
15
|
+
function inventory(root) {
|
|
16
|
+
const entries = [], queue = [root];
|
|
17
|
+
let bytes = 0, visited = 0;
|
|
18
|
+
while (queue.length) {
|
|
19
|
+
const directory = queue.shift();
|
|
20
|
+
if (!fs.lstatSync(directory).isDirectory() || fs.realpathSync(directory) !== directory) fail();
|
|
21
|
+
for (const name of fs.readdirSync(directory).sort()) {
|
|
22
|
+
if (++visited > 128) fail();
|
|
23
|
+
const file = path.join(directory, name), stat = fs.lstatSync(file);
|
|
24
|
+
if (stat.isSymbolicLink()) fail();
|
|
25
|
+
if (stat.isDirectory()) { queue.push(file); continue; }
|
|
26
|
+
if (!stat.isFile() || stat.size > 16 * 1024 * 1024 || (bytes += stat.size) > 32 * 1024 * 1024) fail();
|
|
27
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
28
|
+
try {
|
|
29
|
+
const opened = fs.fstatSync(fd);
|
|
30
|
+
if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino || opened.size !== stat.size) fail();
|
|
31
|
+
const digest = createHash('sha256').update(fs.readFileSync(fd)).digest('hex');
|
|
32
|
+
entries.push([path.relative(root, file), digest, opened.mode & 0o777]);
|
|
33
|
+
} finally { fs.closeSync(fd); }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return JSON.stringify(entries.sort((a, b) => a[0].localeCompare(b[0])));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function companyTerminalRuntime({ platform = process.platform, arch = process.arch,
|
|
40
|
+
bubblewrap = BWRAP, resolve = specifier => createRequire(require.resolve('@lydell/node-pty')).resolve(specifier) } = {}) {
|
|
41
|
+
if (!['linux', 'darwin'].includes(platform)) throw new Error('managed-terminal-platform-unavailable');
|
|
42
|
+
if (platform === 'linux' && !bubblewrap) throw new Error('managed-terminal-bubblewrap-unavailable');
|
|
43
|
+
if (!['arm64', 'x64'].includes(arch)) throw new Error('managed-terminal-platform-unavailable');
|
|
44
|
+
const name = `@lydell/node-pty-${platform}-${arch}`;
|
|
45
|
+
let entry;
|
|
46
|
+
try { entry = fs.realpathSync(resolve(name)); }
|
|
47
|
+
catch { throw new Error('managed-terminal-native-unavailable'); }
|
|
48
|
+
const root = path.dirname(path.dirname(entry));
|
|
49
|
+
if (entry !== path.join(root, 'lib/index.js')) fail();
|
|
50
|
+
const pinned = inventory(root);
|
|
51
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
52
|
+
if (manifest.name !== name || manifest.version !== VERSION || manifest.dependencies && Object.keys(manifest.dependencies).length) fail();
|
|
53
|
+
if (!fs.statSync(path.join(root, `prebuilds/${platform}-${arch}/pty.node`)).isFile()) fail();
|
|
54
|
+
return Object.freeze({ entry, root, version: VERSION,
|
|
55
|
+
assertCurrent() { if (inventory(root) !== pinned) throw new Error('managed-terminal-runtime-changed'); } });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function companyTerminalDiagnostic(options = {}) {
|
|
59
|
+
let reasonCode = null;
|
|
60
|
+
try { companyTerminalRuntime(options); }
|
|
61
|
+
catch (error) { reasonCode = /^managed-terminal-/.test(error.message) ? error.message : 'managed-terminal-runtime-invalid'; }
|
|
62
|
+
return { supported: reasonCode === null, reasonCode, platform: options.platform ?? process.platform,
|
|
63
|
+
scope: 'company', probe: 'runtime-files', ptySpawnVerified: false };
|
|
64
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { stopProcessGroup } from './background-process.mjs';
|
|
3
|
+
import { ProcessTreeWatch } from '../runtime/process-tree-watch.mjs';
|
|
4
|
+
|
|
5
|
+
const watch = new ProcessTreeWatch();
|
|
6
|
+
|
|
7
|
+
// macOS: Seatbelt denies creating a PTY, so the company Gateway makes it and
|
|
8
|
+
// only the shell runs sandboxed, under the agent's offline profile plus these
|
|
9
|
+
// rules: ioctls on terminal devices (only the one this shell was handed can be
|
|
10
|
+
// reached, since opening any other stays denied) and /dev/tty, which always
|
|
11
|
+
// names the process's own terminal.
|
|
12
|
+
export const SEATBELT_TERMINAL_RULES = `
|
|
13
|
+
(allow file-ioctl (regex #"^/dev/ttys[0-9]+$"))
|
|
14
|
+
(allow file-read* file-write* file-ioctl (literal "/dev/tty"))
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export function seatbeltTerminal({ entry, profile, cwd, env, size, emit }) {
|
|
18
|
+
const pty = createRequire(import.meta.url)(entry);
|
|
19
|
+
const child = pty.spawn('/usr/bin/sandbox-exec', ['-p', profile, '/bin/bash', '--noprofile', '--norc'],
|
|
20
|
+
{ cwd, env, cols: size.cols, rows: size.rows, name: 'xterm-256color' });
|
|
21
|
+
watch.watch(child.pid);
|
|
22
|
+
const notify = message => { try { emit(message); } catch { /* consumer never owns process lifetime */ } };
|
|
23
|
+
let exited = false, closing, resolveClosed;
|
|
24
|
+
const completed = new Promise(resolve => { resolveClosed = resolve; });
|
|
25
|
+
child.onData(text => {
|
|
26
|
+
for (let start = 0; start < text.length;) {
|
|
27
|
+
let end = Math.min(start + 8192, text.length);
|
|
28
|
+
if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
|
|
29
|
+
notify({ kind: 'output', data: text.slice(start, end) }); start = end;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
child.onExit(({ exitCode, signal }) => {
|
|
33
|
+
exited = true; notify({ kind: 'exit', exitCode, signal: signal || null });
|
|
34
|
+
void watch.stop(child.pid).finally(resolveClosed);
|
|
35
|
+
});
|
|
36
|
+
const close = () => closing ??= (async () => {
|
|
37
|
+
if (exited) return;
|
|
38
|
+
await stopProcessGroup({ pid: child.pid, exitCode: null, signalCode: null });
|
|
39
|
+
let timeout;
|
|
40
|
+
try { await Promise.race([completed, new Promise((_, reject) => { timeout = setTimeout(() => reject(Error('managed-terminal-cleanup-unconfirmed')), 3000); })]); }
|
|
41
|
+
finally { clearTimeout(timeout); }
|
|
42
|
+
})();
|
|
43
|
+
return { initialized: Promise.resolve(), completed, close, execute(payload) {
|
|
44
|
+
if (exited || closing) return Promise.reject(Error('managed-terminal-exited'));
|
|
45
|
+
if (payload.kind === 'input') child.write(payload.data); else child.resize(payload.cols, payload.rows);
|
|
46
|
+
return Promise.resolve();
|
|
47
|
+
} };
|
|
48
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
4
|
+
|
|
5
|
+
// Only the boundary launches this entrypoint. Check both namespaces before
|
|
6
|
+
// loading native code: accidentally invoking it on the host must fail closed.
|
|
7
|
+
const [entry, hostPidNamespace, hostNetworkNamespace, columns, rows] = process.argv.slice(2);
|
|
8
|
+
const size = (cols, rows) => Number.isInteger(cols) && cols >= 2 && cols <= 500 && Number.isInteger(rows) && rows >= 1 && rows <= 200;
|
|
9
|
+
const send = message => {
|
|
10
|
+
if (process.stdout.writableLength > 512 * 1024) process.exit(1);
|
|
11
|
+
process.stdout.write(JSON.stringify(message) + '\n');
|
|
12
|
+
};
|
|
13
|
+
let terminal;
|
|
14
|
+
try {
|
|
15
|
+
if (process.platform !== 'linux' || !/^pid:\[\d+\]$/.test(hostPidNamespace ?? '') || !/^net:\[\d+\]$/.test(hostNetworkNamespace ?? '')
|
|
16
|
+
|| fs.readlinkSync('/proc/self/ns/pid') === hostPidNamespace || fs.readlinkSync('/proc/self/ns/net') === hostNetworkNamespace
|
|
17
|
+
|| !size(Number(columns), Number(rows))) throw Error('sandbox-required');
|
|
18
|
+
const pty = createRequire(import.meta.url)(entry);
|
|
19
|
+
terminal = pty.spawn('/bin/bash', ['--noprofile', '--norc'], { cols: Number(columns), rows: Number(rows),
|
|
20
|
+
cwd: process.cwd(), env: { ...process.env, TERM: 'xterm-256color' }, name: 'xterm-256color' });
|
|
21
|
+
} catch {
|
|
22
|
+
process.stdout.write(JSON.stringify({ kind: 'unavailable', reasonCode: 'managed-terminal-sandbox-unavailable' }) + '\n', () => process.exit(1));
|
|
23
|
+
}
|
|
24
|
+
if (terminal) {
|
|
25
|
+
let ending = false;
|
|
26
|
+
const finish = message => {
|
|
27
|
+
if (ending) return;
|
|
28
|
+
ending = true;
|
|
29
|
+
// Exiting the namespace's main process also stops detached shell children.
|
|
30
|
+
const timer = setTimeout(() => process.exit(0), 1000); timer.unref();
|
|
31
|
+
process.stdout.write(JSON.stringify(message) + '\n', () => process.exit(0));
|
|
32
|
+
};
|
|
33
|
+
// A writer faster than the Gateway reads (`yes`) waits on the terminal, as
|
|
34
|
+
// on a real one, instead of growing this pipe until the worker gives up.
|
|
35
|
+
let paused = false;
|
|
36
|
+
process.stdout.on('drain', () => { if (paused && !ending) { paused = false; terminal.resume(); } });
|
|
37
|
+
terminal.onData(text => {
|
|
38
|
+
for (let start = 0; start < text.length;) {
|
|
39
|
+
let end = Math.min(start + 8192, text.length);
|
|
40
|
+
if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
|
|
41
|
+
send({ kind: 'output', data: text.slice(start, end) }); start = end;
|
|
42
|
+
}
|
|
43
|
+
if (!paused && process.stdout.writableLength > 128 * 1024) { paused = true; terminal.pause(); }
|
|
44
|
+
});
|
|
45
|
+
terminal.onExit(({ exitCode, signal }) => finish({ kind: 'exit', exitCode, signal: signal ?? null }));
|
|
46
|
+
send({ kind: 'ready' });
|
|
47
|
+
let pending = '';
|
|
48
|
+
const decoder = new StringDecoder('utf8');
|
|
49
|
+
process.stdin.on('data', chunk => {
|
|
50
|
+
pending += decoder.write(chunk);
|
|
51
|
+
if (Buffer.byteLength(pending) > 128 * 1024) return process.exit(1);
|
|
52
|
+
for (;;) {
|
|
53
|
+
const end = pending.indexOf('\n'); if (end < 0) break;
|
|
54
|
+
const line = pending.slice(0, end); pending = pending.slice(end + 1);
|
|
55
|
+
try {
|
|
56
|
+
const request = JSON.parse(line);
|
|
57
|
+
if (!request || typeof request !== 'object' || !/^[0-9]{1,16}$/.test(request.id) || ending) throw Error();
|
|
58
|
+
const keys = Object.keys(request).sort().join(',');
|
|
59
|
+
if (request.kind === 'input' && keys === 'data,id,kind' && typeof request.data === 'string' && Buffer.byteLength(request.data) <= 16384)
|
|
60
|
+
terminal.write(request.data);
|
|
61
|
+
else if (request.kind === 'resize' && keys === 'cols,id,kind,rows' && size(request.cols, request.rows)) terminal.resize(request.cols, request.rows);
|
|
62
|
+
else throw Error();
|
|
63
|
+
send({ kind: 'ack', id: request.id });
|
|
64
|
+
} catch { return process.exit(1); }
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
process.stdin.on('end', () => finish({ kind: 'exit', exitCode: null, signal: null }));
|
|
68
|
+
process.stdin.on('error', () => process.exit(1));
|
|
69
|
+
process.stdout.on('error', () => process.exit(1));
|
|
70
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { consumeReviewSnapshot } from './review-snapshot-request.mjs';
|
|
2
|
+
import { SessionMutationObservations } from "./session-mutation-observations.mjs";
|
|
3
|
+
import { BoundaryTerminals } from './terminal-boundary.mjs';
|
|
4
|
+
import { ToolFreshness } from './tool-freshness.mjs';
|
|
5
|
+
import { BoundaryQueue } from './boundary-queue.mjs';
|
|
6
|
+
import { workspaceWorkerPins, verifyWorkspaceWorker } from './workspace-worker-pins.mjs';
|
|
1
7
|
import fs from 'node:fs';
|
|
2
8
|
import os from 'node:os';
|
|
3
9
|
import path from 'node:path';
|
|
@@ -11,10 +17,11 @@ import { languageEnvironment } from './language-environment.mjs';
|
|
|
11
17
|
import { PI_HOST_VERSION, hostPackage } from './pi-host.mjs';
|
|
12
18
|
export { PI_HOST_VERSION, hostPackage };
|
|
13
19
|
import { containerEngine, dockerCli, engineEnvironment } from './container-engine.mjs';
|
|
14
|
-
import {
|
|
20
|
+
import { proxyEnvironment } from './package-proxy.mjs';
|
|
21
|
+
import { startBoundaryPackageProxy } from './boundary-package-proxy.mjs';
|
|
22
|
+
import { SourceRevertRequests } from './source-revert-requests.mjs';
|
|
15
23
|
import { BWRAP, bubblewrapArgs, credentialFiles, referenceFolders } from './linux-sandbox.mjs';
|
|
16
24
|
import { agentWatchDataDirectory } from './store.mjs';
|
|
17
|
-
|
|
18
25
|
const TOOL_NAMES = Object.freeze(['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls']);
|
|
19
26
|
const MAX_WIRE_BYTES = 12 * 1024 * 1024;
|
|
20
27
|
const workerPath = fileURLToPath(new URL('./tool-worker.mjs', import.meta.url));
|
|
@@ -25,7 +32,6 @@ function literal(value) {
|
|
|
25
32
|
}
|
|
26
33
|
function inside(file, root) { return file === root || file.startsWith(root + path.sep); }
|
|
27
34
|
const pattern = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
-
|
|
29
35
|
// Folders a member points the agent to for reference (an @ mention, "read
|
|
30
36
|
// ~/Documents/old-shop"): their own files on this Mac, external drives and
|
|
31
37
|
// the shared folder, read-only. Hidden entries of the home folder (shell
|
|
@@ -41,7 +47,6 @@ function referenceRules(home, roots) {
|
|
|
41
47
|
return `(allow file-read* (subpath ${literal(home)}) (subpath "/Volumes") (subpath "/Users/Shared") (subpath "/private/tmp"))
|
|
42
48
|
(deny file-read-data file-read-xattr (require-all (require-any (regex #"^${pattern(home)}/\\.") (subpath ${literal(path.join(home, 'Library'))}))${opened}))`;
|
|
43
49
|
}
|
|
44
|
-
|
|
45
50
|
export function managedSeatbelt({ cwd, sdkRoot, runtimeRoot, temporary, node, denied = [], readOnly = false, repository = [], gitDirs = [], toolchains = [], developer = [], browsers = [], network = false, references = null, skills = [], caches = [], loopback = true, license = null, docker = null, proxyPort = null }) {
|
|
46
51
|
// Default-deny protects Keychain/SSH agent/Watch IPC and process inspection.
|
|
47
52
|
// System libraries/toolchains are read-only. Writes stay in this project and
|
|
@@ -110,7 +115,6 @@ ${denied.map(root => `(deny file-read* file-write* (require-all (subpath ${liter
|
|
|
110
115
|
(require-not (regex #"(^|/)\\.env\\.(example|sample|template|dist|defaults)$"))))
|
|
111
116
|
`;
|
|
112
117
|
}
|
|
113
|
-
|
|
114
118
|
// Toolchains from the pinned node/git and Homebrew, compilers and SDK; the
|
|
115
119
|
// git identity this project gets on the user's machine (name and email only)
|
|
116
120
|
// so commits work. Without network, package managers fail fast instead of
|
|
@@ -192,7 +196,13 @@ function provisionSearchTools(home, userHome) {
|
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
export class ManagedToolBoundary {
|
|
195
|
-
#
|
|
199
|
+
#queue = new BoundaryQueue();
|
|
200
|
+
#freshness = new ToolFreshness();
|
|
201
|
+
#sourceRequests = new SourceRevertRequests();
|
|
202
|
+
get sourceControlRevision() { return this.#sourceRequests.revision; }
|
|
203
|
+
source(request, binding) { return this.invoke("workspace", this.#sourceRequests.request(request, binding)); }
|
|
204
|
+
#mutationObservations = new SessionMutationObservations();
|
|
205
|
+
mutationObservation(file) { return this.#mutationObservations.get(file); }
|
|
196
206
|
#children = new Set();
|
|
197
207
|
#closed = false;
|
|
198
208
|
// readOnly: the project cannot be written. commands: bash is offered all
|
|
@@ -205,10 +215,11 @@ export class ManagedToolBoundary {
|
|
|
205
215
|
this.cwd = fs.realpathSync(cwd); this.sdkRoot = fs.realpathSync(sdkRoot);
|
|
206
216
|
this.node = fs.realpathSync(node);
|
|
207
217
|
this.runtimeRoot = fs.realpathSync(path.dirname(workerPath));
|
|
218
|
+
this.workspacePins = workspaceWorkerPins(this.runtimeRoot);
|
|
208
219
|
this.sdkPath = path.join(this.sdkRoot, 'dist/index.js');
|
|
209
220
|
this.git = managedGit();
|
|
210
221
|
if (JSON.parse(fs.readFileSync(path.join(this.sdkRoot, 'package.json'), 'utf8')).version !== PI_HOST_VERSION) throw new Error('managed-sdk-version-unqualified');
|
|
211
|
-
const workerModules = ['search-fallback.mjs', 'sandbox-diagnostic.mjs', 'ts-diagnostics-server.mjs'].map(name => path.join(this.runtimeRoot, name));
|
|
222
|
+
const workerModules = ['review-snapshot-worker.mjs', 'tool-freshness.mjs', 'search-fallback.mjs', 'sandbox-diagnostic.mjs', 'ts-diagnostics-server.mjs', 'workspace-worker-pins.mjs'].map(name => path.join(this.runtimeRoot, name));
|
|
212
223
|
this.pins = new Map([this.node, this.git, workerPath, ...workerModules, this.sdkPath].map(file => [file, hash(file)]));
|
|
213
224
|
const userHome = fs.realpathSync(home ?? os.homedir());
|
|
214
225
|
if ([userHome, '/', this.sdkRoot, this.runtimeRoot].includes(this.cwd)
|
|
@@ -260,6 +271,7 @@ export class ManagedToolBoundary {
|
|
|
260
271
|
if (searchTools) provisionSearchTools(this.home, userHome);
|
|
261
272
|
else fs.writeFileSync(path.join(this.home, '.ripgreprc'), '', { mode: 0o600 });
|
|
262
273
|
this.allowed = readOnly ? TOOL_NAMES.filter(name => !['write', 'edit'].includes(name) && (name !== 'bash' || commands)) : [...TOOL_NAMES];
|
|
274
|
+
this.terminals = new BoundaryTerminals(this);
|
|
263
275
|
}
|
|
264
276
|
// What the machine offers commands: toolchains, the JDK, the Docker engine.
|
|
265
277
|
// Looked up again after the member installs or starts something
|
|
@@ -288,6 +300,8 @@ export class ManagedToolBoundary {
|
|
|
288
300
|
if (this.engine) this.environment.docker = { ...this.environment.network, ...engineEnvironment(this.engine, this.home) };
|
|
289
301
|
this.#applyProxy();
|
|
290
302
|
}
|
|
303
|
+
// A company terminal's shell (terminal-seatbelt.mjs): offline, its own temporary root.
|
|
304
|
+
terminalProfile(temporary) { return managedSeatbelt({ ...this.base, temporary, references: this.userHome, loopback: false }); }
|
|
291
305
|
// Plain commands' profile and environment, with the package proxy once it listens.
|
|
292
306
|
#applyProxy() {
|
|
293
307
|
const port = this.proxyUrl && !this.linux ? Number(new URL(this.proxyUrl).port) : null;
|
|
@@ -313,34 +327,20 @@ export class ManagedToolBoundary {
|
|
|
313
327
|
const factories = { read: api.createReadToolDefinition, write: api.createWriteToolDefinition,
|
|
314
328
|
edit: api.createEditToolDefinition, bash: api.createBashToolDefinition,
|
|
315
329
|
grep: api.createGrepToolDefinition, find: api.createFindToolDefinition, ls: api.createLsToolDefinition };
|
|
316
|
-
return this.allowed.map(name => ({ ...factories[name](this.cwd), execute: (_id, args, signal, onUpdate, context) =>
|
|
317
|
-
|
|
318
|
-
// observe a deterministic order. Helpers use separate read-only workers.
|
|
319
|
-
const run = this.#tail.then(() => this.invoke(name, args, signal, onUpdate, context?.model));
|
|
320
|
-
this.#tail = run.catch(() => {}); return run;
|
|
321
|
-
} }));
|
|
330
|
+
return this.allowed.map(name => ({ ...factories[name](this.cwd), execute: (_id, args, signal, onUpdate, context) =>
|
|
331
|
+
this.invoke(name, args, signal, onUpdate, context?.model) }));
|
|
322
332
|
}
|
|
323
333
|
// The proxy listens on loopback (macOS: Seatbelt opens its one port even
|
|
324
334
|
// while a local proxy closes the rest of loopback) or on a Unix socket that
|
|
325
335
|
// the worker forwards from inside the command's network namespace (Linux).
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
let url;
|
|
330
|
-
if (this.linux) {
|
|
331
|
-
this.proxySocket = path.join(os.tmpdir(), `piagent-proxy-${randomUUID().slice(0, 8)}.sock`);
|
|
332
|
-
await this.proxy.listen(this.proxySocket);
|
|
333
|
-
url = 'http://127.0.0.1:3128';
|
|
334
|
-
} else url = `http://127.0.0.1:${(await this.proxy.listen()).port}`;
|
|
335
|
-
const settings = path.join(this.home, '.m2/settings.xml');
|
|
336
|
-
fs.mkdirSync(path.dirname(settings), { recursive: true, mode: 0o700 }); fs.writeFileSync(settings, mavenSettings(url), { mode: 0o600 });
|
|
337
|
-
this.proxyUrl = url; this.#applyProxy();
|
|
338
|
-
} catch { await this.proxy.close().catch(() => {}); this.proxy = null; }
|
|
336
|
+
startProxy() { return startBoundaryPackageProxy(this, () => this.#applyProxy()); }
|
|
337
|
+
invoke(name, args, signal, onUpdate, model, options = {}) {
|
|
338
|
+
return this.#queue.run(name, args, () => this.#queuedInvoke(name, args, signal, onUpdate, model, options));
|
|
339
339
|
}
|
|
340
|
-
async
|
|
340
|
+
async #queuedInvoke(name, args, signal, onUpdate, model, { network = false, docker = false, guard = undefined } = {}) {
|
|
341
341
|
await this.proxyReady;
|
|
342
342
|
const before = this.proxy?.deniedCount ?? 0;
|
|
343
|
-
try { return await this.#invoke(name, args, signal, onUpdate, model, { network, docker }); }
|
|
343
|
+
try { guard?.(); return await this.#invoke(name, args, signal, onUpdate, model, { network, docker, guard }); }
|
|
344
344
|
catch (error) {
|
|
345
345
|
// A command that failed after the proxy refused a host: name the hosts.
|
|
346
346
|
const refused = !network && !docker && this.proxy ? this.proxy.deniedSince(before) : [];
|
|
@@ -348,10 +348,11 @@ export class ManagedToolBoundary {
|
|
|
348
348
|
throw new Error(`managed-domain-blocked: Lệnh thường chỉ tải được từ các registry package quen thuộc (npm, PyPI, Go, crates.io, Maven, Gradle, NuGet, RubyGems, Packagist, pub.dev…) và tải về public từ GitHub. Host bị chặn: ${refused.join(', ')}. Nếu lệnh thật sự cần host này, dùng run_with_network để người dùng duyệt đúng lệnh đó.\n${String(error?.message ?? error)}`);
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
|
-
async #invoke(name, args, signal, onUpdate, model, { network = false, docker = false } = {}) {
|
|
351
|
+
async #invoke(name, args, signal, onUpdate, model, { network = false, docker = false, guard = undefined } = {}) {
|
|
352
352
|
if (docker && !this.engine) throw new Error('managed-tool-unavailable');
|
|
353
353
|
network ||= docker;
|
|
354
|
-
if (
|
|
354
|
+
if (name === 'workspace') { if (network || docker) throw Error('managed-tool-unavailable'); verifyWorkspaceWorker(this.workspacePins); }
|
|
355
|
+
if (this.#closed || (name !== 'workspace' && !this.allowed.includes(name)) || network && (name !== 'bash' || !this.networkAllowed)) throw new Error('managed-tool-unavailable');
|
|
355
356
|
if (signal?.aborted) throw new Error('managed-tool-cancelled');
|
|
356
357
|
for (const [file, digest] of this.pins) if (hash(file) !== digest) throw new Error('managed-runtime-changed');
|
|
357
358
|
// `~` (as in an @~/… mention) is the member's home folder: the sandbox's
|
|
@@ -359,9 +360,15 @@ export class ManagedToolBoundary {
|
|
|
359
360
|
if (name !== 'bash' && typeof args?.path === 'string' && /^@?~(\/|$)/.test(args.path)) args = { ...args, path: this.userHome + args.path.replace(/^@?~/, '') };
|
|
360
361
|
if (!this.linux && !network && Date.now() - this.proxyCheck.at > 5000) this.proxyCheck = { at: Date.now(), listening: localProxyListening(this.proxyPorts) };
|
|
361
362
|
const isolated = !this.linux && !network && await this.proxyCheck.listening;
|
|
362
|
-
const
|
|
363
|
+
const observing = ['edit', 'write'].includes(name) && this.workspacePins;
|
|
364
|
+
if (observing) verifyWorkspaceWorker(this.workspacePins);
|
|
365
|
+
const snapshotRead = name === 'bash' && consumeReviewSnapshot(args);
|
|
366
|
+
const mutationObservation = snapshotRead ? undefined : this.#mutationObservations.prepare(this.cwd, name, args, observing);
|
|
367
|
+
const sourceContext = this.#sourceRequests.consume(args, this.#mutationObservations);
|
|
368
|
+
const message = JSON.stringify({ name, args, sourceContext, mutationObservation, expectedFileDigest: this.#freshness.expected(this.cwd, name, args), workspaceReadOnly: this.setup.readOnly, ...(network ? { network: true } : {}), ...(isolated ? { isolated: true } : {}), ...(docker ? { docker: true } : this.engine ? { engine: true } : {}), model: model ? { input: model.input, inputLimits: model.inputLimits } : undefined }) + '\n';
|
|
363
369
|
if (Buffer.byteLength(message) > MAX_WIRE_BYTES) throw new Error('managed-tool-input-too-large');
|
|
364
370
|
return await new Promise((resolve, reject) => {
|
|
371
|
+
guard?.();
|
|
365
372
|
const [sandbox, sandboxArgs] = this.linux ? [BWRAP, this.bubblewrap(network, docker)]
|
|
366
373
|
: ['/usr/bin/sandbox-exec', ['-p', docker ? this.dockerProfile : network ? this.networkProfile : isolated ? this.isolatedProfile : this.profile]];
|
|
367
374
|
const child = spawn(sandbox, [...sandboxArgs, this.node, workerPath, this.sdkPath, this.cwd], {
|
|
@@ -370,7 +377,7 @@ export class ManagedToolBoundary {
|
|
|
370
377
|
});
|
|
371
378
|
this.#children.add(child);
|
|
372
379
|
const decoder = new StringDecoder('utf8');
|
|
373
|
-
let pending = '', bytes = 0, result, failure, terminated = false, hardStop;
|
|
380
|
+
let pending = '', bytes = 0, result, freshness, observation, sourceObservation, failure, terminated = false, hardStop;
|
|
374
381
|
const terminate = () => {
|
|
375
382
|
if (terminated) return; terminated = true;
|
|
376
383
|
child.stdin.write('{"cancel":true}\n');
|
|
@@ -405,7 +412,7 @@ export class ManagedToolBoundary {
|
|
|
405
412
|
// A cancel or timeout asked for first stays the reason: the worker
|
|
406
413
|
// then reports how its command ended ("Command aborted").
|
|
407
414
|
else if (value.error) failure ??= new Error(String(value.error).slice(0, 1000));
|
|
408
|
-
else if (value.result) result = value.result;
|
|
415
|
+
else if (value.result) { result = value.result; freshness = value.freshness; observation = value.mutationObservation; sourceObservation = value.sourceObservation; }
|
|
409
416
|
else throw new Error('managed-worker-invalid-response');
|
|
410
417
|
} catch { failure = new Error('managed-worker-invalid-response'); terminate(); }
|
|
411
418
|
}
|
|
@@ -419,7 +426,9 @@ export class ManagedToolBoundary {
|
|
|
419
426
|
this.#children.delete(child); signal?.removeEventListener('abort', abort);
|
|
420
427
|
if (failure) reject(failure);
|
|
421
428
|
else if (code !== 0 || !result || pending) reject(new Error('managed-worker-failed'));
|
|
422
|
-
else
|
|
429
|
+
else { this.#freshness.observe(this.cwd, name, args, freshness);
|
|
430
|
+
this.#mutationObservations.acceptRevert(args, sourceContext, result, sourceObservation);
|
|
431
|
+
this.#mutationObservations.accept(this.cwd, name, args, mutationObservation, observation); resolve(result); }
|
|
423
432
|
});
|
|
424
433
|
child.stdin.write(message);
|
|
425
434
|
if (signal?.aborted) abort();
|
|
@@ -462,8 +471,9 @@ export class ManagedToolBoundary {
|
|
|
462
471
|
async dispose() {
|
|
463
472
|
this.#closed = true;
|
|
464
473
|
const stopping = [...this.#children].map(child => new Promise(resolve => { child.once('close', resolve); child.managedCancel(); }));
|
|
465
|
-
await Promise.all(stopping); await this.#
|
|
474
|
+
await Promise.all([this.terminals.dispose(), ...stopping]); await this.#queue.idle();
|
|
466
475
|
await this.background?.dispose(); await this.stopStrays(); if (!this.linux) await stopMarked(`PIAGENT_PROCESS_SCOPE=${this.scope}:`);
|
|
476
|
+
this.#mutationObservations.clear();
|
|
467
477
|
await this.proxyReady; await this.proxy?.close().catch(() => {});
|
|
468
478
|
if (this.proxySocket) fs.rmSync(this.proxySocket, { force: true });
|
|
469
479
|
fs.rmSync(this.temporary, { recursive: true, force: true });
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const LIMIT = 20 * 1024 * 1024;
|
|
6
|
+
export function freshnessPath(cwd, name, args) {
|
|
7
|
+
if (!['read', 'edit', 'write'].includes(name) || typeof args?.path !== 'string') return null;
|
|
8
|
+
const relative = path.relative(cwd, path.resolve(cwd, args.path));
|
|
9
|
+
return relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) ? relative : null;
|
|
10
|
+
}
|
|
11
|
+
// Called only inside the sandbox. The parent stores opaque digests and never
|
|
12
|
+
// opens a company project file to decide whether an edit is fresh.
|
|
13
|
+
export function fileDigest(cwd, relative) {
|
|
14
|
+
if (!relative) return null;
|
|
15
|
+
let descriptor;
|
|
16
|
+
try {
|
|
17
|
+
const file = path.resolve(cwd, relative), real = fs.realpathSync(file);
|
|
18
|
+
if (real !== file) return null;
|
|
19
|
+
descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
20
|
+
const stat = fs.fstatSync(descriptor);
|
|
21
|
+
if (!stat.isFile() || stat.size > LIMIT) return null;
|
|
22
|
+
const bytes = Buffer.alloc(stat.size + 1), length = fs.readSync(descriptor, bytes, 0, bytes.length, 0), after = fs.fstatSync(descriptor);
|
|
23
|
+
if (length !== stat.size || after.size !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs) return null;
|
|
24
|
+
return createHash('sha256').update(bytes.subarray(0, length)).digest('hex');
|
|
25
|
+
} catch (error) { return error?.code === 'ENOENT' ? 'absent' : null; }
|
|
26
|
+
finally { if (descriptor !== undefined) fs.closeSync(descriptor); }
|
|
27
|
+
}
|
|
28
|
+
export function beginToolFreshness(cwd, name, args, expected) {
|
|
29
|
+
const relative = freshnessPath(cwd, name, args), before = fileDigest(cwd, relative);
|
|
30
|
+
if (relative && ['edit', 'write'].includes(name) && expected !== undefined && before !== expected)
|
|
31
|
+
throw new Error(`stale-content: ${relative} changed since the last read. Read its current contents before editing.`);
|
|
32
|
+
return () => {
|
|
33
|
+
const digest = fileDigest(cwd, relative);
|
|
34
|
+
if (name === 'read' && relative && before !== digest) throw new Error(`stale-content: ${relative} changed during the read. Read it again.`);
|
|
35
|
+
return relative && digest ? { path: relative, digest } : null;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export class ToolFreshness {
|
|
39
|
+
#observed = new Map();
|
|
40
|
+
expected(cwd, name, args) { return this.#observed.get(freshnessPath(cwd, name, args)); }
|
|
41
|
+
observe(cwd, name, args, value) {
|
|
42
|
+
const relative = freshnessPath(cwd, name, args);
|
|
43
|
+
if (!relative || value?.path !== relative || !/^(absent|[a-f0-9]{64})$/.test(value?.digest ?? '')) return;
|
|
44
|
+
this.#observed.delete(relative); this.#observed.set(relative, value.digest);
|
|
45
|
+
while (this.#observed.size > 256) this.#observed.delete(this.#observed.keys().next().value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { beginToolFreshness } from './tool-freshness.mjs';
|
|
1
2
|
import { sandboxDiagnostic } from './sandbox-diagnostic.mjs';
|
|
2
3
|
import { fallbackFind, fallbackGrep } from './search-fallback.mjs';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { createInterface } from 'node:readline';
|
|
6
|
-
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
8
|
// A dedicated, credential-free process already inside Seatbelt. The launch
|
|
8
9
|
// profile and environment are supplied by the trusted runtime, never the LLM.
|
|
9
10
|
const [sdkPath, cwd] = process.argv.slice(2);
|
|
@@ -33,8 +34,22 @@ input.on('line', async line => {
|
|
|
33
34
|
try {
|
|
34
35
|
const message = JSON.parse(line);
|
|
35
36
|
if (message.cancel === true) { controller?.abort(); return; }
|
|
36
|
-
if (active || !Object.hasOwn(factories, message.name)) throw new Error('managed-worker-request-invalid');
|
|
37
|
+
if (active || (message.name !== 'workspace' && !Object.hasOwn(factories, message.name))) throw new Error('managed-worker-request-invalid');
|
|
37
38
|
active = true; controller = new AbortController(); network = message.network === true; isolated = message.isolated === true; docker = message.docker === true; engine = message.engine === true;
|
|
39
|
+
if (message.name === 'workspace') {
|
|
40
|
+
const { inspectManagedWorkspace, inspectManagedSource } = await import('./dist/workspace/worker.mjs');
|
|
41
|
+
const assets = fileURLToPath(new URL('./dist/workspace/assets/', import.meta.url));
|
|
42
|
+
emit(message.args?.kind === 'source'
|
|
43
|
+
? await inspectManagedSource(cwd, assets, message.args.request, message.sourceContext, message.workspaceReadOnly === true)
|
|
44
|
+
: { result: await inspectManagedWorkspace(cwd, assets, message.args, message.workspaceReadOnly === true) }); return;
|
|
45
|
+
}
|
|
46
|
+
let finishObservation = () => null;
|
|
47
|
+
if (message.mutationObservation && ['edit', 'write'].includes(message.name)) {
|
|
48
|
+
const { beginManagedMutationObservation } = await import('./dist/workspace/worker.mjs');
|
|
49
|
+
finishObservation = beginManagedMutationObservation(cwd, fileURLToPath(new URL('./dist/workspace/assets/', import.meta.url)),
|
|
50
|
+
{ ...message.mutationObservation, toolName: message.name, args: message.args }, message.workspaceReadOnly === true);
|
|
51
|
+
}
|
|
52
|
+
const finishFreshness = beginToolFreshness(cwd, message.name, message.args, message.expectedFileDigest);
|
|
38
53
|
const options = message.name === 'bash' ? { shellPath: '/bin/bash', exposeSessionEnvironment: false } : undefined;
|
|
39
54
|
// The runtime copies ripgrep/fd here when the machine has or can fetch them.
|
|
40
55
|
const provided = name => fs.existsSync(path.join(process.env.HOME ?? '', '.pi/agent/bin', name));
|
|
@@ -46,7 +61,7 @@ input.on('line', async line => {
|
|
|
46
61
|
// failures. Pi 0.87 throws on denial and on a non-zero shell exit; Pi 1.x
|
|
47
62
|
// returns an isError result with the same text instead: read as the throw.
|
|
48
63
|
if (result?.isError === true) throw new Error((result.content ?? []).filter(part => part.type === 'text').map(part => part.text).join('\n') || 'managed-worker-failed');
|
|
49
|
-
emit({ result });
|
|
64
|
+
emit({ result, freshness: finishFreshness(), mutationObservation: finishObservation() });
|
|
50
65
|
} catch (error) { emit({ error: sandboxDiagnostic(error instanceof Error ? error.message : 'managed-worker-failed', { network, isolated, docker, engine }) }); }
|
|
51
66
|
finally { if (active) { input.close(); process.stdin.destroy(); } }
|
|
52
67
|
});
|
|
@@ -2,6 +2,9 @@ import dns from 'node:dns/promises';
|
|
|
2
2
|
import https from 'node:https';
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import zlib from 'node:zlib';
|
|
5
|
+
import { privateAddress } from './ip-policy.mjs';
|
|
6
|
+
|
|
7
|
+
export { privateAddress };
|
|
5
8
|
|
|
6
9
|
// Reads one public https page for the company session, from the runtime
|
|
7
10
|
// process (tools in the sandbox have no network). GET only, no cookies or
|
|
@@ -11,20 +14,6 @@ const MAX_BYTES = 5 * 1024 * 1024;
|
|
|
11
14
|
const MAX_REDIRECTS = 5;
|
|
12
15
|
const TEXT = /^(text\/|application\/(json|xml|xhtml\+xml|javascript|x-yaml|yaml|ld\+json|rss\+xml|atom\+xml))/i;
|
|
13
16
|
|
|
14
|
-
function v4Private(address) {
|
|
15
|
-
const [a, b] = address.split('.').map(Number);
|
|
16
|
-
return a === 0 || a === 10 || a === 127 || a >= 224 || (a === 100 && b >= 64 && b <= 127) || (a === 169 && b === 254)
|
|
17
|
-
|| (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 192 && b === 0) || (a === 198 && (b === 18 || b === 19));
|
|
18
|
-
}
|
|
19
|
-
export function privateAddress(address) {
|
|
20
|
-
if (net.isIPv4(address)) return v4Private(address);
|
|
21
|
-
if (!net.isIPv6(address)) return true;
|
|
22
|
-
const value = address.toLowerCase();
|
|
23
|
-
const mapped = value.match(/^(?:0*:)*:ffff:(\d+\.\d+\.\d+\.\d+)$/) ?? value.match(/^64:ff9b::(\d+\.\d+\.\d+\.\d+)$/);
|
|
24
|
-
if (mapped) return v4Private(mapped[1]);
|
|
25
|
-
return value === '::' || value === '::1' || /^f[cd]/.test(value) || /^fe[89ab]/.test(value) || /^ff/.test(value) || value.startsWith('2001:db8');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
17
|
async function publicTarget(url, resolve) {
|
|
29
18
|
if (url.protocol !== 'https:' || url.username || url.password) throw new Error('web-fetch-url-not-allowed');
|
|
30
19
|
const host = url.hostname.replace(/^\[|\]$/g, '');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
const digest = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
5
|
+
|
|
6
|
+
export function workspaceWorkerPins(runtimeRoot) {
|
|
7
|
+
const root = path.join(runtimeRoot, 'dist/workspace'), manifest = path.join(root, 'manifest.json');
|
|
8
|
+
if (!fs.existsSync(manifest)) return null;
|
|
9
|
+
const source = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
10
|
+
if (source.version !== 1 || !Array.isArray(source.files) || source.files.length > 200) throw Error('workspace-worker-invalid');
|
|
11
|
+
const pins = new Map([[manifest, digest(manifest)]]);
|
|
12
|
+
for (const item of source.files) {
|
|
13
|
+
if (typeof item.file !== 'string' || !/^[a-zA-Z0-9_./-]+$/.test(item.file)
|
|
14
|
+
|| item.file.split('/').some(part => !part || part === '.' || part === '..') || !/^[a-f0-9]{64}$/.test(item.sha256)) throw Error('workspace-worker-invalid');
|
|
15
|
+
const file = path.join(root, item.file);
|
|
16
|
+
if (fs.realpathSync(file) !== file || !fs.statSync(file).isFile() || digest(file) !== item.sha256) throw Error('workspace-worker-invalid');
|
|
17
|
+
pins.set(file, item.sha256);
|
|
18
|
+
}
|
|
19
|
+
if (!pins.has(path.join(root, 'worker.mjs'))) throw Error('workspace-worker-invalid');
|
|
20
|
+
return pins;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function verifyWorkspaceWorker(pins) {
|
|
24
|
+
if (!pins) throw Error('workspace-worker-unavailable');
|
|
25
|
+
for (const [file, expected] of pins) if (digest(file) !== expected) throw Error('managed-runtime-changed');
|
|
26
|
+
}
|