@piagent/platform 1.37.1 → 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 +47 -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 +2 -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 +17 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +17 -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 +18 -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 +18 -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/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 +2 -2
- package/packages/piagent-core/managed/source-revert-requests.mjs +21 -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/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,36 @@
|
|
|
1
|
+
import { sessionRefForPath } from "./session-catalog.ts";
|
|
2
|
+
import type { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
|
|
3
|
+
|
|
4
|
+
type Options = {
|
|
5
|
+
key: Buffer;
|
|
6
|
+
managed: boolean;
|
|
7
|
+
gatewayInstanceRef: string;
|
|
8
|
+
runtimes: Pick<SessionRuntimeSupervisor, "listSessions" | "acquire" | "liveSessionManager" | "ownership">;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Browser refs select catalog entries, never host paths or shell environments.
|
|
12
|
+
// Keep a synchronous authority check for the instant before each native effect.
|
|
13
|
+
export async function personalTerminalBinding(options: Options, sessionRef: string) {
|
|
14
|
+
if (options.managed) throw new Error("company-boundary-required");
|
|
15
|
+
const { runtimes } = options;
|
|
16
|
+
const matches = (await runtimes.listSessions()).filter(info => sessionRefForPath(options.key, info.path) === sessionRef);
|
|
17
|
+
if (matches.length !== 1) throw new Error("terminal-session-unavailable");
|
|
18
|
+
const info = matches[0]!;
|
|
19
|
+
const lease = await runtimes.acquire(sessionRef);
|
|
20
|
+
const manager = runtimes.liveSessionManager(sessionRef);
|
|
21
|
+
const runtimeInstanceRef = lease.runtimeInstanceRef;
|
|
22
|
+
if (!runtimeInstanceRef || !manager || manager.getSessionId() !== info.id)
|
|
23
|
+
throw new Error("terminal-session-unavailable");
|
|
24
|
+
const assertCurrent = () => {
|
|
25
|
+
const current = runtimes.liveSessionManager(sessionRef), ownership = runtimes.ownership(sessionRef);
|
|
26
|
+
if (current !== manager || current.getSessionId() !== info.id || ownership.state !== "gateway-owned"
|
|
27
|
+
|| ownership.owner.continuity !== "exact" || ownership.owner.gatewayInstanceRef !== options.gatewayInstanceRef
|
|
28
|
+
|| ownership.owner.runtimeInstanceRef !== runtimeInstanceRef || ownership.owner.ownerEpoch !== lease.ownerEpoch)
|
|
29
|
+
throw new Error("terminal-runtime-replaced");
|
|
30
|
+
if (current.buildSessionContext().model?.provider === "agent_watch_managed")
|
|
31
|
+
throw new Error("company-boundary-required");
|
|
32
|
+
};
|
|
33
|
+
assertCurrent();
|
|
34
|
+
return { scope: { sessionRef, mode: "personal" as const, cwd: info.cwd },
|
|
35
|
+
identity: { gatewayInstanceRef: options.gatewayInstanceRef, sessionRef, runtimeInstanceRef }, assertCurrent };
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
export function loadPersonalPty(load = () => require('@lydell/node-pty'), platform = process.platform) {
|
|
4
|
+
if (platform === 'win32') return { pty: null, reason: 'terminal-platform-unavailable' };
|
|
5
|
+
try {
|
|
6
|
+
const pty = load();
|
|
7
|
+
if (typeof pty?.spawn !== 'function') throw Error('invalid-pty-module');
|
|
8
|
+
return { pty, reason: null };
|
|
9
|
+
} catch { return { pty: null, reason: 'terminal-native-module-unavailable' }; }
|
|
10
|
+
}
|
|
11
|
+
export function personalTerminalDiagnostic(load, platform = process.platform) {
|
|
12
|
+
const { reason } = loadPersonalPty(load, platform);
|
|
13
|
+
return { available: reason === null, reasonCode: reason, platform,
|
|
14
|
+
scope: 'personal', nativeModule: '@lydell/node-pty', probe: 'module-load',
|
|
15
|
+
shellSpawnVerified: false };
|
|
16
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { loadPersonalPty } from "./personal-terminal-diagnostic.mjs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { userInfo } from "node:os";
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import { isAbsolute } from "node:path";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import type { IPty } from "@lydell/node-pty";
|
|
8
|
+
import { ProcessTreeWatch } from "../../piagent-core/runtime/process-tree-watch.mjs";
|
|
9
|
+
|
|
10
|
+
type PtyModule = { spawn: typeof import("@lydell/node-pty").spawn };
|
|
11
|
+
type Scope = { sessionRef: string; mode: "personal" | "company"; cwd: string };
|
|
12
|
+
type Size = { cols: number; rows: number };
|
|
13
|
+
export type PersonalTerminalEvent = { sessionRef: string; terminalRef: string } &
|
|
14
|
+
({ kind: "output"; data: string } | { kind: "exit"; exitCode: number; signal: number | null });
|
|
15
|
+
type Entry = { sessionRef: string; terminalRef: string; process: IPty; exited: boolean; cleanup?: Promise<void>; closing?: Promise<void> };
|
|
16
|
+
const dimensions = ({ cols, rows }: Size) => {
|
|
17
|
+
if (!Number.isInteger(cols) || cols < 2 || cols > 500 || !Number.isInteger(rows) || rows < 1 || rows > 200)
|
|
18
|
+
throw new Error("terminal-invalid-size");
|
|
19
|
+
};
|
|
20
|
+
function descendants(pid: number): number[] {
|
|
21
|
+
const rows = execFileSync("/bin/ps", ["-axo", "pid=,ppid="], { encoding: "utf8", timeout: 2000, maxBuffer: 4 * 1024 * 1024 })
|
|
22
|
+
.trim().split("\n").map(line => line.trim().split(/\s+/).map(Number));
|
|
23
|
+
const found = new Set<number>(), queue = [pid];
|
|
24
|
+
for (const parent of queue) for (const [child, owner] of rows) {
|
|
25
|
+
if (owner === parent && child > 1 && child !== process.pid && !found.has(child)) { found.add(child); queue.push(child); }
|
|
26
|
+
}
|
|
27
|
+
return [...found].reverse();
|
|
28
|
+
}
|
|
29
|
+
const signal = (pid: number, name: NodeJS.Signals) => { try { process.kill(pid, name); } catch { /* already exited */ } };
|
|
30
|
+
|
|
31
|
+
// This owns personal native processes only. The protocol/controller must resolve
|
|
32
|
+
// the trusted session scope before calling it; company never falls back here.
|
|
33
|
+
export class PersonalTerminalProcesses {
|
|
34
|
+
readonly #entries = new Map<string, Entry>();
|
|
35
|
+
readonly #closingSessions = new Set<string>();
|
|
36
|
+
readonly #emit: (event: PersonalTerminalEvent) => void;
|
|
37
|
+
readonly #pty: PtyModule | null;
|
|
38
|
+
readonly #watch = new ProcessTreeWatch();
|
|
39
|
+
readonly unavailableReason: string | null;
|
|
40
|
+
#closed = false;
|
|
41
|
+
constructor(emit: (event: PersonalTerminalEvent) => void, load?: () => PtyModule) {
|
|
42
|
+
this.#emit = emit;
|
|
43
|
+
const { pty, reason } = loadPersonalPty(load);
|
|
44
|
+
this.#pty = pty; this.unavailableReason = reason;
|
|
45
|
+
}
|
|
46
|
+
open(scope: Scope, size: Size, shell = userInfo().shell || process.env.SHELL || "/bin/sh") {
|
|
47
|
+
if (scope.mode !== "personal") throw new Error("company-boundary-required");
|
|
48
|
+
if (this.#closed || this.#closingSessions.has(scope.sessionRef)) throw new Error("terminal-session-closed");
|
|
49
|
+
if (!this.#pty) throw new Error(this.unavailableReason!);
|
|
50
|
+
dimensions(size);
|
|
51
|
+
if (!isAbsolute(scope.cwd) || !statSync(scope.cwd, { throwIfNoEntry: false })?.isDirectory()) throw new Error("terminal-project-unavailable");
|
|
52
|
+
if (!isAbsolute(shell) || shell.includes("\0")) throw new Error("terminal-shell-unavailable");
|
|
53
|
+
if (this.list(scope.sessionRef).length >= 4 || this.#entries.size >= 256) throw new Error("terminal-limit");
|
|
54
|
+
const terminalRef = `terminal.${randomUUID()}`;
|
|
55
|
+
// The member's own environment, without what only this Gateway uses
|
|
56
|
+
// (its agent folder, transform root, update switches).
|
|
57
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(PI|PIAGENT)_/.test(key)));
|
|
58
|
+
let child: IPty;
|
|
59
|
+
try { child = this.#pty.spawn(shell, [], { ...size, cwd: scope.cwd, env: { ...env, TERM: "xterm-256color" }, name: "xterm-256color" }); }
|
|
60
|
+
catch { throw new Error("terminal-spawn-failed"); }
|
|
61
|
+
this.#watch.watch(child.pid);
|
|
62
|
+
const entry: Entry = { sessionRef: scope.sessionRef, terminalRef, process: child, exited: false };
|
|
63
|
+
this.#entries.set(terminalRef, entry);
|
|
64
|
+
child.onData(data => { if (!entry.closing) this.#notify({ sessionRef: scope.sessionRef, terminalRef, kind: "output", data }); });
|
|
65
|
+
child.onExit(({ exitCode, signal: exitSignal }) => {
|
|
66
|
+
entry.exited = true;
|
|
67
|
+
entry.cleanup ??= this.#stop(entry);
|
|
68
|
+
this.#notify({ sessionRef: scope.sessionRef, terminalRef, kind: "exit", exitCode, signal: exitSignal ?? null });
|
|
69
|
+
});
|
|
70
|
+
return { terminalRef, pid: child.pid };
|
|
71
|
+
}
|
|
72
|
+
list(sessionRef: string) { return [...this.#entries.values()].filter(entry => entry.sessionRef === sessionRef)
|
|
73
|
+
.map(entry => ({ terminalRef: entry.terminalRef, exited: entry.exited })); }
|
|
74
|
+
input(sessionRef: string, terminalRef: string, data: string) {
|
|
75
|
+
if (typeof data !== "string" || Buffer.byteLength(data) > 16 * 1024) throw new Error("terminal-input-limit");
|
|
76
|
+
const entry = this.#get(sessionRef, terminalRef);
|
|
77
|
+
if (entry.exited || entry.closing) throw new Error("terminal-exited");
|
|
78
|
+
entry.process.write(data);
|
|
79
|
+
}
|
|
80
|
+
resize(sessionRef: string, terminalRef: string, size: Size) {
|
|
81
|
+
dimensions(size); const entry = this.#get(sessionRef, terminalRef);
|
|
82
|
+
if (entry.exited || entry.closing) throw new Error("terminal-exited");
|
|
83
|
+
entry.process.resize(size.cols, size.rows);
|
|
84
|
+
}
|
|
85
|
+
pause(sessionRef: string, terminalRef: string) { const entry = this.#get(sessionRef, terminalRef); if (!entry.exited) entry.process.pause(); }
|
|
86
|
+
resume(sessionRef: string, terminalRef: string) { const entry = this.#get(sessionRef, terminalRef); if (!entry.exited) entry.process.resume(); }
|
|
87
|
+
async close(sessionRef: string, terminalRef: string) {
|
|
88
|
+
const entry = this.#get(sessionRef, terminalRef);
|
|
89
|
+
if (!entry.closing) { entry.cleanup ??= this.#stop(entry); entry.closing = entry.cleanup.finally(() => this.#entries.delete(terminalRef)); }
|
|
90
|
+
await entry.closing;
|
|
91
|
+
}
|
|
92
|
+
async closeSession(sessionRef: string) {
|
|
93
|
+
this.#closingSessions.add(sessionRef);
|
|
94
|
+
try { await Promise.all(this.list(sessionRef).map(entry => this.close(sessionRef, entry.terminalRef))); }
|
|
95
|
+
finally { this.#closingSessions.delete(sessionRef); }
|
|
96
|
+
}
|
|
97
|
+
async dispose() {
|
|
98
|
+
this.#closed = true;
|
|
99
|
+
await Promise.all([...new Set([...this.#entries.values()].map(entry => entry.sessionRef))].map(ref => this.closeSession(ref)));
|
|
100
|
+
}
|
|
101
|
+
#get(sessionRef: string, terminalRef: string) {
|
|
102
|
+
const entry = this.#entries.get(terminalRef);
|
|
103
|
+
if (!entry || entry.sessionRef !== sessionRef) throw new Error("terminal-not-found");
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
#notify(event: PersonalTerminalEvent) { try { this.#emit(event); } catch { /* a disconnected consumer cannot own process lifetime */ } }
|
|
107
|
+
async #stop(entry: Entry) {
|
|
108
|
+
const pid = entry.process.pid;
|
|
109
|
+
let tree: number[] = [];
|
|
110
|
+
try { if (!entry.exited) tree = descendants(pid); } catch { /* still signal the PTY process group */ }
|
|
111
|
+
for (const child of tree) signal(child, "SIGTERM");
|
|
112
|
+
try { if (!entry.exited) entry.process.kill("SIGHUP"); } catch { /* already exited */ }
|
|
113
|
+
signal(-pid, "SIGTERM");
|
|
114
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
115
|
+
for (const child of tree) signal(child, "SIGKILL");
|
|
116
|
+
signal(-pid, "SIGKILL");
|
|
117
|
+
await this.#watch.stop(pid);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { personalTerminalBinding } from "./personal-terminal-binding.ts";
|
|
2
|
+
import { PersonalTerminalProcesses, type PersonalTerminalEvent } from "./personal-terminal-processes.ts";
|
|
3
|
+
import { TerminalSessions } from "./terminal-sessions.ts";
|
|
4
|
+
export type { TerminalMessage as PersonalTerminalMessage } from "./terminal-sessions.ts";
|
|
5
|
+
type Binding = Awaited<ReturnType<typeof personalTerminalBinding>>;
|
|
6
|
+
type Options = Parameters<typeof personalTerminalBinding>[0];
|
|
7
|
+
|
|
8
|
+
// Personal authority is resolved here. This adapter never accepts a company boundary.
|
|
9
|
+
export class PersonalTerminalSessions extends TerminalSessions<Binding> {
|
|
10
|
+
constructor(options: Options, createProcesses = (emit: (event: PersonalTerminalEvent) => void) => new PersonalTerminalProcesses(emit)) {
|
|
11
|
+
super(ref => personalTerminalBinding(options, ref), emit => {
|
|
12
|
+
const processes = createProcesses(emit);
|
|
13
|
+
return { reason: () => processes.unavailableReason,
|
|
14
|
+
open: (binding, size) => processes.open(binding.scope, size),
|
|
15
|
+
input: (ref, terminal, data) => processes.input(ref, terminal, data),
|
|
16
|
+
resize: (ref, terminal, size) => processes.resize(ref, terminal, size),
|
|
17
|
+
close: (ref, terminal) => processes.close(ref, terminal),
|
|
18
|
+
pause: (ref, terminal) => processes.pause(ref, terminal), resume: (ref, terminal) => processes.resume(ref, terminal),
|
|
19
|
+
closeSession: ref => processes.closeSession(ref), dispose: () => processes.dispose() };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -14,11 +14,12 @@ type RuntimeCommandOutput = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export async function executePermissionCommand(session: any,
|
|
17
|
-
permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<void> {
|
|
17
|
+
permissionMode: "read-only" | "workspace-write" | "trusted-full-access" | "bypass"): Promise<void> {
|
|
18
18
|
// A company conversation: "Ask first" or "Bypass" (no read-only mode); the
|
|
19
19
|
// runtime keeps the choice in the conversation and reads it at each command,
|
|
20
20
|
// so it may change while a turn runs (the supervisor lets it).
|
|
21
21
|
if (session.managedExecution) {
|
|
22
|
+
// Bypass where the team's Harness allows it; the runtime refuses otherwise.
|
|
22
23
|
if (typeof session.managedSetPermission !== "function" || permissionMode === "read-only") throw new Error("managed-permission-unavailable");
|
|
23
24
|
session.managedSetPermission(permissionMode);
|
|
24
25
|
return;
|
|
@@ -49,7 +49,7 @@ function catalogRef(key: Buffer, namespace: string, value: string): string {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export type SessionOwnerProjection = Pick<SessionRow, "state" | "liveState" | "composerAvailable" | "needsAttention" | "owner" | "reasonCode">;
|
|
52
|
-
export type SessionOptionProjection = Pick<SessionRow, "modelLabel" | "thinkingLevel" | "managedHelpers" | "managedThinkingLevels" | "managedPermission" | "managedFast" | "managedPlan" | "managedProcess" | "managedTurnEnd" | "managedReview">;
|
|
52
|
+
export type SessionOptionProjection = Pick<SessionRow, "modelLabel" | "thinkingLevel" | "managedHelpers" | "managedThinkingLevels" | "managedPermission" | "managedBypass" | "managedFast" | "managedPlan" | "managedProcess" | "managedTurnEnd" | "managedReview">;
|
|
53
53
|
|
|
54
54
|
function display(value: unknown, maximum: number, fallback: string): string {
|
|
55
55
|
const clean = redactSensitiveText(String(value ?? "")).text
|
|
@@ -136,6 +136,7 @@ function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undef
|
|
|
136
136
|
...(sessionOptions?.managedThinkingLevels ? {managedThinkingLevels:sessionOptions.managedThinkingLevels} : {}),
|
|
137
137
|
...(sessionOptions?.managedPermission ? {managedPermission:sessionOptions.managedPermission} : {}),
|
|
138
138
|
...(sessionOptions?.managedFast ? {managedFast:sessionOptions.managedFast} : {}),
|
|
139
|
+
...(sessionOptions?.managedBypass ? {managedBypass:true as const} : {}),
|
|
139
140
|
...(sessionOptions?.managedPlan ? {managedPlan:sessionOptions.managedPlan} : {}),
|
|
140
141
|
...(sessionOptions?.managedProcess ? {managedProcess:sessionOptions.managedProcess} : {}),
|
|
141
142
|
...(sessionOptions?.managedTurnEnd ? {managedTurnEnd:sessionOptions.managedTurnEnd} : {}),
|
|
@@ -148,7 +149,7 @@ function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undef
|
|
|
148
149
|
? { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "released" }
|
|
149
150
|
: ownership?.owner ?? { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "unknown" },
|
|
150
151
|
sessionRevision: revision(key, [info.path, info.modified.toISOString(), info.messageCount, info.name ?? null, metadata?.revision ?? null,
|
|
151
|
-
sessionOptions?.modelLabel ?? null, sessionOptions?.thinkingLevel ?? "unknown", sessionOptions?.managedHelpers, sessionOptions?.managedThinkingLevels, sessionOptions?.managedPermission ?? null, sessionOptions?.managedFast ?? null, sessionOptions?.managedPlan, sessionOptions?.managedProcess, sessionOptions?.managedTurnEnd, sessionOptions?.managedReview,
|
|
152
|
+
sessionOptions?.modelLabel ?? null, sessionOptions?.thinkingLevel ?? "unknown", sessionOptions?.managedHelpers, sessionOptions?.managedThinkingLevels, sessionOptions?.managedPermission ?? null, sessionOptions?.managedBypass ?? null, sessionOptions?.managedFast ?? null, sessionOptions?.managedPlan, sessionOptions?.managedProcess, sessionOptions?.managedTurnEnd, sessionOptions?.managedReview,
|
|
152
153
|
gitBranch, gitRepositories,
|
|
153
154
|
metadata?.archived ? "archived" : ownership ? [ownership.state, ownership.liveState, ownership.owner.kind,
|
|
154
155
|
ownership.owner.ownerEpoch, ownership.owner.runtimeInstanceRef, ownership.reasonCode] : "offline"]),
|
|
@@ -57,7 +57,7 @@ function validPayload(action: SessionAction, payload: Record<string, unknown>):
|
|
|
57
57
|
if (action === "session.set-thinking") return exactKeys(payload, ["thinkingLevel"])
|
|
58
58
|
&& ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(payload.thinkingLevel));
|
|
59
59
|
if (action === "session.set-permission") return exactKeys(payload, ["permissionMode"])
|
|
60
|
-
&& ["read-only", "workspace-write", "trusted-full-access"].includes(String(payload.permissionMode));
|
|
60
|
+
&& ["read-only", "workspace-write", "trusted-full-access", "bypass"].includes(String(payload.permissionMode));
|
|
61
61
|
if (action === "session.set-fast") return exactKeys(payload, ["fast"]) && typeof payload.fast === "boolean";
|
|
62
62
|
if (action === "session.rename") return exactKeys(payload, ["title"]) && typeof payload.title === "string"
|
|
63
63
|
&& payload.title.length >= 1 && payload.title.length <= 500 && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(payload.title);
|
|
@@ -71,7 +71,7 @@ function validPayload(action: SessionAction, payload: Record<string, unknown>):
|
|
|
71
71
|
&& ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(payload.thinkingLevel))
|
|
72
72
|
&& typeof payload.message === "string" && payload.message.length >= 1 && payload.message.length <= 32_768 && !payload.message.includes("\0")
|
|
73
73
|
&& REF.test(String(payload.messageRequestId)) && (payload.deferInitialMessage === undefined || typeof payload.deferInitialMessage === "boolean")
|
|
74
|
-
&& (payload.permissionMode === undefined || ["read-only", "workspace-write", "trusted-full-access"].includes(String(payload.permissionMode)))
|
|
74
|
+
&& (payload.permissionMode === undefined || ["read-only", "workspace-write", "trusted-full-access", "bypass"].includes(String(payload.permissionMode)))
|
|
75
75
|
&& (payload.fast === undefined || typeof payload.fast === "boolean")
|
|
76
76
|
&& (payload.workflow === undefined || isWebUiWorkflowId(payload.workflow));
|
|
77
77
|
}
|
|
@@ -180,7 +180,7 @@ export class SessionCommandController {
|
|
|
180
180
|
}
|
|
181
181
|
if (command.payload.permissionMode !== undefined) {
|
|
182
182
|
await this.#runtimes.setPermission(targetSessionRef, String(command.payload.permissionMode) as
|
|
183
|
-
"read-only" | "workspace-write" | "trusted-full-access");
|
|
183
|
+
"read-only" | "workspace-write" | "trusted-full-access" | "bypass");
|
|
184
184
|
}
|
|
185
185
|
// Fast mode chosen on the new chat page is on before the first message
|
|
186
186
|
// runs; a Harness that does not allow it refuses the create.
|
|
@@ -232,7 +232,7 @@ export class SessionCommandController {
|
|
|
232
232
|
resultCode = await this.#runtimes.setThinking(command.sessionRef!, String(command.payload.thinkingLevel));
|
|
233
233
|
} else if (command.action === "session.set-permission") {
|
|
234
234
|
resultCode = await this.#runtimes.setPermission(command.sessionRef!, String(command.payload.permissionMode) as
|
|
235
|
-
"read-only" | "workspace-write" | "trusted-full-access");
|
|
235
|
+
"read-only" | "workspace-write" | "trusted-full-access" | "bypass");
|
|
236
236
|
} else if (command.action === "session.set-fast") {
|
|
237
237
|
resultCode = await this.#runtimes.setFast(command.sessionRef!, command.payload.fast === true);
|
|
238
238
|
} else if (command.action === "session.rename") {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { piSourceMutationGuard } from "../../piagent-core/runtime/policy/source-mutation-guard.ts";
|
|
2
|
+
import { personalWorkspaceReader } from "../server/workspace-reader.ts";
|
|
1
3
|
import { createHash } from "node:crypto";
|
|
2
4
|
import fs from "node:fs";
|
|
3
5
|
import path from "node:path";
|
|
@@ -49,7 +51,7 @@ const EMPTY_REPLAY = {
|
|
|
49
51
|
reasonCode: null
|
|
50
52
|
};
|
|
51
53
|
|
|
52
|
-
function inspectionRuntimeRef(gatewayInstanceRef: string, sessionRef: string): string {
|
|
54
|
+
export function inspectionRuntimeRef(gatewayInstanceRef: string, sessionRef: string): string {
|
|
53
55
|
return `runtime.${createHash("sha256").update(`${gatewayInstanceRef}\0${sessionRef}`).digest("hex")}`;
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -118,13 +120,16 @@ export class SessionInspectionRegistry {
|
|
|
118
120
|
readonly #packageRoot: string;
|
|
119
121
|
readonly #agentDir?: string;
|
|
120
122
|
readonly #models?: ModelRuntime;
|
|
123
|
+
readonly #managed: boolean;
|
|
124
|
+
readonly #workspaceRead?: (sessionRef: string, id: string, kind: string, path: string) => Promise<unknown>;
|
|
121
125
|
readonly #projects?: ProjectSource;
|
|
122
126
|
readonly #mcpAuth?: McpAuthBroker;
|
|
123
127
|
readonly #operationLiveness: ((sessionRef: string) => "idle" | "running" | "unknown") | null;
|
|
124
128
|
readonly #providers = new Map<string, { modifiedAt: string; provider: CoreInspectionProvider }>();
|
|
125
129
|
|
|
126
130
|
constructor(options: { gatewayInstanceRef: string; host: PiHost; key: Buffer; packageRoot: string; agentDir?: string; models?: ModelRuntime; projects?: ProjectSource;
|
|
127
|
-
|
|
131
|
+
workspaceRead?: (sessionRef: string, id: string, kind: string, path: string) => Promise<unknown>;
|
|
132
|
+
managed?: boolean; mcpAuth?: McpAuthBroker; listSessions?: () => Promise<PiSessionInfo[]>;
|
|
128
133
|
openLiveSession?: (sessionRef: string) => ReturnType<PiHost["SessionManager"]["open"]> | null;
|
|
129
134
|
operationLiveness?: (sessionRef: string) => "idle" | "running" | "unknown" }) {
|
|
130
135
|
this.#gatewayInstanceRef = options.gatewayInstanceRef;
|
|
@@ -135,6 +140,8 @@ export class SessionInspectionRegistry {
|
|
|
135
140
|
this.#packageRoot = options.packageRoot;
|
|
136
141
|
this.#agentDir = options.agentDir;
|
|
137
142
|
this.#models = options.models;
|
|
143
|
+
this.#managed = options.managed === true;
|
|
144
|
+
this.#workspaceRead = options.workspaceRead;
|
|
138
145
|
this.#projects = options.projects;
|
|
139
146
|
this.#mcpAuth = options.mcpAuth;
|
|
140
147
|
this.#operationLiveness = options.operationLiveness ?? null;
|
|
@@ -225,7 +232,7 @@ export class SessionInspectionRegistry {
|
|
|
225
232
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_048_576) return [];
|
|
226
233
|
const value = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
|
|
227
234
|
return [{ id: entry, displayName: safeName(value.displayName ?? entry),
|
|
228
|
-
permissionMode: ["read-only", "workspace-write", "trusted-full-access"].includes(String(value.permissionProfile))
|
|
235
|
+
permissionMode: ["read-only", "workspace-write", "trusted-full-access", "bypass"].includes(String(value.permissionProfile))
|
|
229
236
|
? String(value.permissionProfile) : null }];
|
|
230
237
|
}).slice(0, 100), []);
|
|
231
238
|
const models = availableModels.flatMap((value) => {
|
|
@@ -239,7 +246,8 @@ export class SessionInspectionRegistry {
|
|
|
239
246
|
const inputs = Array.isArray(value.input) ? value.input.filter((item): item is string => typeof item === "string") : null;
|
|
240
247
|
return [{ modelRef: webUiModelRef(provider, modelId), provider, modelId, displayName: safeName(value.name ?? modelId),
|
|
241
248
|
reasoning: value.reasoning, imageInput: inputs ? inputs.includes("image") : null, thinkingLevels,
|
|
242
|
-
...(provider === "agent_watch_managed" && value.managedFast === true ? { managedFast: true } : {})
|
|
249
|
+
...(provider === "agent_watch_managed" && value.managedFast === true ? { managedFast: true } : {}),
|
|
250
|
+
...(provider === "agent_watch_managed" && value.managedBypass === true ? { managedBypass: true } : {}) }];
|
|
243
251
|
});
|
|
244
252
|
// Only offer a default present in the authenticated host catalog. Retain
|
|
245
253
|
// the previous Sol baseline for accounts that have not received GPT-6 yet.
|
|
@@ -256,7 +264,7 @@ export class SessionInspectionRegistry {
|
|
|
256
264
|
reasonCode: projects.size ? null : "no-known-project" };
|
|
257
265
|
}
|
|
258
266
|
|
|
259
|
-
async provider(sessionRef: string): Promise<
|
|
267
|
+
async provider(sessionRef: string): Promise<CoreInspectionProvider> {
|
|
260
268
|
const info = await this.#sessionInfo(sessionRef);
|
|
261
269
|
const modifiedAt = info.modified.toISOString();
|
|
262
270
|
const cached = this.#providers.get(sessionRef);
|
|
@@ -288,6 +296,11 @@ export class SessionInspectionRegistry {
|
|
|
288
296
|
runtimeInstanceId: inspectionRuntimeRef(this.#gatewayInstanceRef, sessionRef),
|
|
289
297
|
eventStore,
|
|
290
298
|
task: () => safeRead(() => activeSessionTask(info.cwd, info.id), undefined),
|
|
299
|
+
...(!this.#managed && context.model?.provider !== "agent_watch_managed" ? {
|
|
300
|
+
sourceMutationGuardAvailable: () => piSourceMutationGuard.available(info.cwd, info.id),
|
|
301
|
+
sourceSessionControlRevision: () => piSourceMutationGuard.sessionControlRevision(info.cwd, info.id),
|
|
302
|
+
sourceSessionObservation: (file: string) => piSourceMutationGuard.sessionObservation(info.cwd, info.id, file)
|
|
303
|
+
} : {}),
|
|
291
304
|
activityEvents: () => safeRead(() => readContextTelemetry(info.cwd, { limit: 5_000 }), []),
|
|
292
305
|
// A provider can outlive the turn that created it (notably while a new
|
|
293
306
|
// session has not reached the host index yet). Read the branch at request
|
|
@@ -312,6 +325,17 @@ export class SessionInspectionRegistry {
|
|
|
312
325
|
approvalProjection: () => piApprovalBroker.projection(info.cwd, info.id),
|
|
313
326
|
approvalDetail: (approvalRef) => piApprovalBroker.detail(info.cwd, info.id, approvalRef)
|
|
314
327
|
});
|
|
328
|
+
if (this.#managed && this.#workspaceRead) {
|
|
329
|
+
Object.assign(provider, { workspace: async (kind: string, relative: string) => {
|
|
330
|
+
const value = await this.#workspaceRead!(sessionRef, info.id, kind, relative);
|
|
331
|
+
return { schemaVersion: 1, version: `piagent-webui-workspace-${kind}-v1`, generatedAt: new Date().toISOString(),
|
|
332
|
+
identity: (await provider.snapshot() as { identity: unknown }).identity, ...(value as object) };
|
|
333
|
+
} });
|
|
334
|
+
}
|
|
335
|
+
if (!this.#managed && context.model?.provider !== "agent_watch_managed") {
|
|
336
|
+
Object.assign(provider, { workspace: personalWorkspaceReader({ cwd: info.cwd, packageRoot: this.#packageRoot,
|
|
337
|
+
identity: async () => (await provider.snapshot() as { identity: Record<string, unknown> }).identity }) });
|
|
338
|
+
}
|
|
315
339
|
this.#providers.delete(sessionRef);
|
|
316
340
|
this.#providers.set(sessionRef, { modifiedAt, provider });
|
|
317
341
|
while (this.#providers.size > CACHE_LIMIT) this.#providers.delete(this.#providers.keys().next().value as string);
|
|
@@ -32,22 +32,21 @@ export class SessionRuntimeSupervisor {
|
|
|
32
32
|
readonly #runtimeFactory: RuntimeFactory; readonly #events: GatewayEventStore; readonly #settledOwners = new Set<string>();
|
|
33
33
|
readonly #host: any | null; readonly #operationDeadlinePolicy: SessionOperationDeadlinePolicy;
|
|
34
34
|
readonly #resolveProject: ((projectRef: string) => string | null) | null;
|
|
35
|
-
readonly #sessionDirectory?: string;
|
|
35
|
+
readonly #sessionDirectory?: string; readonly #closeSessionResources: (sessionRef: string) => Promise<void>;
|
|
36
36
|
readonly #settleOperation: ReturnType<typeof createSessionOperationSettlement>;
|
|
37
37
|
readonly #active = new Map<string, ActiveRuntime>(); readonly #opening = new Map<string, Promise<SessionLeaseSnapshot>>();
|
|
38
38
|
readonly #created = new Map<string, PiSessionInfo>(); #closed = false; #closing: Promise<void> | null = null; #readProjection: ((sessionRef: string) => Promise<Projection>) | null = null;
|
|
39
39
|
constructor(options: {
|
|
40
|
-
gatewayInstanceRef: string;
|
|
41
|
-
key: Buffer;
|
|
40
|
+
gatewayInstanceRef: string; key: Buffer;
|
|
42
41
|
leases: SessionLeaseStore;
|
|
43
42
|
listSessions(): Promise<PiSessionInfo[]>;
|
|
44
43
|
runtimeFactory?: RuntimeFactory;
|
|
45
44
|
host?: any;
|
|
46
|
-
agentDir?: string;
|
|
47
|
-
packageRoot?: string;
|
|
45
|
+
agentDir?: string; packageRoot?: string;
|
|
48
46
|
modelRuntime?: any;
|
|
49
47
|
scopedBrokerRouter?: ScopedBrokerRouter;
|
|
50
48
|
sessionDirectory?: string;
|
|
49
|
+
closeSessionResources?(sessionRef: string): Promise<void>;
|
|
51
50
|
events?: GatewayEventStore;
|
|
52
51
|
operationWatchdog?: SessionOperationWatchdogOptions;
|
|
53
52
|
resolveProject?(projectRef: string): string | null;
|
|
@@ -61,7 +60,7 @@ export class SessionRuntimeSupervisor {
|
|
|
61
60
|
this.#host = options.host ?? null;
|
|
62
61
|
this.#operationDeadlinePolicy = sessionOperationDeadlinePolicy(options.operationWatchdog);
|
|
63
62
|
this.#resolveProject = options.resolveProject ?? null;
|
|
64
|
-
this.#sessionDirectory = options.sessionDirectory;
|
|
63
|
+
this.#sessionDirectory = options.sessionDirectory; this.#closeSessionResources = options.closeSessionResources ?? (async () => {});
|
|
65
64
|
this.#settleOperation = createSessionOperationSettlement(options);
|
|
66
65
|
if (options.runtimeFactory) this.#runtimeFactory = options.runtimeFactory;
|
|
67
66
|
else {
|
|
@@ -169,7 +168,7 @@ export class SessionRuntimeSupervisor {
|
|
|
169
168
|
if (active.operationRef || active.settling) throw new Error("session-runtime-busy");
|
|
170
169
|
this.#active.delete(sessionRef);
|
|
171
170
|
bestEffortUnsubscribe(active.unsubscribeApproval); bestEffortUnsubscribe(active.unbindApproval);
|
|
172
|
-
try { await active.runtime.dispose(); }
|
|
171
|
+
try { await this.#closeSessionResources(sessionRef); await active.runtime.dispose(); }
|
|
173
172
|
catch (error) {
|
|
174
173
|
try { this.#leases.requireRecovery(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef,
|
|
175
174
|
active.lease.runtimeInstanceRef!, "session-runtime-dispose-failed"); } catch { /* remains fail closed */ }
|
|
@@ -308,6 +307,7 @@ export class SessionRuntimeSupervisor {
|
|
|
308
307
|
if (active.lastSessionRevision) this.#events.publish("runtime.changed", { sessionRef,
|
|
309
308
|
sessionRevision: active.lastSessionRevision, liveState: "uncertain", operationRef: null,
|
|
310
309
|
...(messageRequestId ? { messageRequestId } : {}), reasonCode });
|
|
310
|
+
await this.#closeSessionResources(sessionRef);
|
|
311
311
|
await boundedResult(Promise.resolve().then(() => active.runtime.dispose()), this.#operationDeadlinePolicy.terminationTimeoutMs);
|
|
312
312
|
}
|
|
313
313
|
#bindApproval(sessionRef: string, active: ActiveRuntime): void {
|
|
@@ -362,7 +362,7 @@ export class SessionRuntimeSupervisor {
|
|
|
362
362
|
if (effective !== thinkingLevel) throw new Error("session-thinking-mismatch");
|
|
363
363
|
this.#touchCreated(sessionRef); return "thinking-changed";
|
|
364
364
|
}
|
|
365
|
-
async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<"permission-changed"> {
|
|
365
|
+
async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access" | "bypass"): Promise<"permission-changed"> {
|
|
366
366
|
await this.acquire(sessionRef);
|
|
367
367
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
368
368
|
if (!active || !session) throw new Error("session-runtime-unavailable");
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const MAX_BYTES = 8 * 1024 * 1024, MAX_RECORDS = 4000;
|
|
6
|
+
type SessionRecord = { identity: { sessionRef: string }; idempotencyKeyDigest: string };
|
|
7
|
+
type Journal<T> = { version: 1; sequence: number; records: Array<{ sequence: number; receipt: T }> };
|
|
8
|
+
|
|
9
|
+
// Gateway-owned receipts survive a runtime eviction or an empty conversation
|
|
10
|
+
// that Pi has not persisted yet. No source contents or raw idempotency keys.
|
|
11
|
+
export class SignedSessionJournal<T extends SessionRecord> {
|
|
12
|
+
readonly #namespace: string;
|
|
13
|
+
readonly #root: string;
|
|
14
|
+
readonly #key: Buffer;
|
|
15
|
+
readonly #directory: fs.Stats;
|
|
16
|
+
constructor(root: string, key: Buffer, namespace: string) {
|
|
17
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(namespace) || key.length < 32) throw new Error("session-journal-config-invalid");
|
|
18
|
+
this.#namespace = namespace;
|
|
19
|
+
this.#root = path.join(fs.realpathSync(root), namespace); this.#key = Buffer.from(key);
|
|
20
|
+
try { fs.mkdirSync(this.#root, { mode: 0o700 }); }
|
|
21
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
|
|
22
|
+
const stat = fs.lstatSync(this.#root);
|
|
23
|
+
this.#directory = stat; this.#checkDirectory();
|
|
24
|
+
}
|
|
25
|
+
#checkDirectory(): void {
|
|
26
|
+
const stat = fs.lstatSync(this.#root);
|
|
27
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077)
|
|
28
|
+
|| typeof process.getuid === "function" && stat.uid !== process.getuid()
|
|
29
|
+
|| stat.dev !== this.#directory.dev || stat.ino !== this.#directory.ino) throw new Error("workspace-journal-unsafe");
|
|
30
|
+
}
|
|
31
|
+
#file(sessionRef: string): string {
|
|
32
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/.test(sessionRef)) throw new Error("workspace-session-invalid");
|
|
33
|
+
return path.join(this.#root, `${sessionRef}.json`);
|
|
34
|
+
}
|
|
35
|
+
#sign(sessionRef: string, value: Journal<T>): string {
|
|
36
|
+
return createHmac("sha256", this.#key).update(`${this.#namespace}-v1\0${sessionRef}\0`).update(JSON.stringify(value)).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
#read(sessionRef: string): Journal<T> {
|
|
39
|
+
this.#checkDirectory();
|
|
40
|
+
let descriptor: number;
|
|
41
|
+
try { descriptor = fs.openSync(this.#file(sessionRef), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK); }
|
|
42
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return { version: 1, sequence: 0, records: [] }; throw error; }
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.fstatSync(descriptor);
|
|
45
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > MAX_BYTES || (stat.mode & 0o077)
|
|
46
|
+
|| typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new Error("workspace-journal-unsafe");
|
|
47
|
+
const { value, mac } = JSON.parse(fs.readFileSync(descriptor, "utf8")) as { value: Journal<T>; mac: string };
|
|
48
|
+
if (!value || typeof mac !== "string" || !/^[a-f0-9]{64}$/.test(mac) || !timingSafeEqual(Buffer.from(mac, "hex"), Buffer.from(this.#sign(sessionRef, value), "hex"))
|
|
49
|
+
|| value.version !== 1 || !Number.isSafeInteger(value.sequence) || value.sequence < 0 || !Array.isArray(value.records) || value.records.length > MAX_RECORDS
|
|
50
|
+
|| value.records.some(record => !record || !Number.isSafeInteger(record.sequence) || record.sequence < 1 || record.sequence > value.sequence
|
|
51
|
+
|| !record.receipt || record.receipt.identity?.sessionRef !== sessionRef || !/^sha256:[a-f0-9]{64}$/.test(record.receipt.idempotencyKeyDigest)))
|
|
52
|
+
throw new Error("workspace-journal-invalid");
|
|
53
|
+
return value;
|
|
54
|
+
} finally { fs.closeSync(descriptor); }
|
|
55
|
+
}
|
|
56
|
+
history(sessionRef: string): T[] {
|
|
57
|
+
return this.#read(sessionRef).records.sort((a, b) => a.sequence - b.sequence).map(record => record.receipt);
|
|
58
|
+
}
|
|
59
|
+
append(sessionRef: string, receipt: T): void {
|
|
60
|
+
if (receipt.identity.sessionRef !== sessionRef) throw new Error("workspace-session-invalid");
|
|
61
|
+
const value = this.#read(sessionRef), sequence = value.sequence + 1;
|
|
62
|
+
const records = value.records.filter(record => record.receipt.idempotencyKeyDigest !== receipt.idempotencyKeyDigest);
|
|
63
|
+
records.push({ sequence, receipt });
|
|
64
|
+
if (records.length > MAX_RECORDS || !Number.isSafeInteger(sequence)) throw new Error("workspace-journal-full");
|
|
65
|
+
const next: Journal<T> = { version: 1, sequence, records }, bytes = JSON.stringify({ value: next, mac: this.#sign(sessionRef, next) }) + "\n";
|
|
66
|
+
if (Buffer.byteLength(bytes) > MAX_BYTES) throw new Error("workspace-journal-full");
|
|
67
|
+
const temporary = path.join(this.#root, `.write-${randomBytes(16).toString("hex")}`);
|
|
68
|
+
let descriptor: number | undefined;
|
|
69
|
+
try {
|
|
70
|
+
descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
|
|
71
|
+
fs.writeFileSync(descriptor, bytes); fs.fsyncSync(descriptor); fs.closeSync(descriptor); descriptor = undefined;
|
|
72
|
+
this.#checkDirectory(); fs.renameSync(temporary, this.#file(sessionRef));
|
|
73
|
+
const directory = fs.openSync(this.#root, fs.constants.O_RDONLY);
|
|
74
|
+
try { fs.fsyncSync(directory); } finally { fs.closeSync(directory); }
|
|
75
|
+
} finally { if (descriptor !== undefined) fs.closeSync(descriptor); fs.rmSync(temporary, { force: true }); }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { SourceRevertProjection } from "../../piagent-core/runtime/inspection/source-action-contract.ts";
|
|
2
|
+
import { piSourceMutationGuard } from "../../piagent-core/runtime/policy/source-mutation-guard.ts";
|
|
3
|
+
import type { BridgeSnapshot } from "../../piagent-core/runtime/inspection/session-identity.ts";
|
|
4
|
+
import { SourceRevertController } from "../server/source-revert-controller.ts";
|
|
5
|
+
import type { CoreInspectionProvider } from "../server/core-inspection-provider.ts";
|
|
6
|
+
import { SourceRevertStore } from "./source-revert-store.ts";
|
|
7
|
+
import { sessionRefForPath } from "./session-catalog.ts";
|
|
8
|
+
import type { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
|
|
9
|
+
|
|
10
|
+
type Options = { root: string; key: Buffer; managed: boolean;
|
|
11
|
+
runtimes: Pick<SessionRuntimeSupervisor, "listSessions" | "acquire" | "liveSessionManager" | "currentOperation">;
|
|
12
|
+
provider(sessionRef: string): Promise<CoreInspectionProvider>; changed(sessionRef: string): void };
|
|
13
|
+
|
|
14
|
+
// The catalog ref selects a runtime. The inspection identity inside commands
|
|
15
|
+
// remains independently bound to that runtime; the two refs need not be equal.
|
|
16
|
+
export class SourceRevertSessions {
|
|
17
|
+
readonly #options: Options; readonly #store: SourceRevertStore;
|
|
18
|
+
readonly #pending = new Map<string, Promise<unknown>>();
|
|
19
|
+
constructor(options: Options) { this.#options = options; this.#store = new SourceRevertStore(options.root, options.key); }
|
|
20
|
+
#serial<T>(sessionRef: string, action: () => Promise<T>): Promise<T> {
|
|
21
|
+
const run = (this.#pending.get(sessionRef) ?? Promise.resolve()).then(action, action);
|
|
22
|
+
this.#pending.set(sessionRef, run);
|
|
23
|
+
void run.finally(() => { if (this.#pending.get(sessionRef) === run) this.#pending.delete(sessionRef); }).catch(() => {});
|
|
24
|
+
return run;
|
|
25
|
+
}
|
|
26
|
+
async #live(sessionRef: string) {
|
|
27
|
+
const { runtimes, key } = this.#options;
|
|
28
|
+
if (this.#options.managed) throw new Error("source-company-boundary-required");
|
|
29
|
+
const matches = (await runtimes.listSessions()).filter(info => sessionRefForPath(key, info.path) === sessionRef);
|
|
30
|
+
if (matches.length !== 1) throw new Error("source-session-unavailable");
|
|
31
|
+
const info = matches[0]; await runtimes.acquire(sessionRef);
|
|
32
|
+
const manager = runtimes.liveSessionManager(sessionRef);
|
|
33
|
+
if (!manager || manager.getSessionId() !== info.id) throw new Error("source-session-unavailable");
|
|
34
|
+
if (manager.buildSessionContext().model?.provider === "agent_watch_managed") throw new Error("source-company-boundary-required");
|
|
35
|
+
const provider = await this.#options.provider(sessionRef); provider.invalidate();
|
|
36
|
+
const snapshot = await provider.snapshot() as { identity: NonNullable<BridgeSnapshot["identity"]>; revision: NonNullable<BridgeSnapshot["revisions"]> };
|
|
37
|
+
if (snapshot.identity.taskId !== null || snapshot.identity.taskRunId !== null) throw new Error("source-session-binding-unavailable");
|
|
38
|
+
const binding = (): BridgeSnapshot => {
|
|
39
|
+
const current = runtimes.liveSessionManager(sessionRef), same = current === manager && current?.getSessionId() === info.id;
|
|
40
|
+
return { state: same ? "ready" : "replacement-pending", identity: snapshot.identity,
|
|
41
|
+
revisions: { ...snapshot.revision, controlRevision: piSourceMutationGuard.sessionControlRevision(info.cwd, info.id) },
|
|
42
|
+
taskState: "none", liveness: same ? runtimes.currentOperation(sessionRef) ? "running" : "idle" : "unknown", eventSequence: 0 };
|
|
43
|
+
};
|
|
44
|
+
return { info, provider, snapshot, binding };
|
|
45
|
+
}
|
|
46
|
+
sources(sessionRef: string) {
|
|
47
|
+
return this.#serial(sessionRef, async () => (await this.#live(sessionRef)).provider.sourceChanges("working-tree"));
|
|
48
|
+
}
|
|
49
|
+
diff(sessionRef: string, fileRef: string) {
|
|
50
|
+
return this.#serial(sessionRef, async () => (await this.#live(sessionRef)).provider.diff("working-tree", fileRef));
|
|
51
|
+
}
|
|
52
|
+
preview(sessionRef: string, fileRef: string, hunkRef: string | null) {
|
|
53
|
+
return this.#serial(sessionRef, async () => {
|
|
54
|
+
const { provider } = await this.#live(sessionRef);
|
|
55
|
+
const preview = await provider.sourceRevert(fileRef, hunkRef) as SourceRevertProjection;
|
|
56
|
+
const snapshot = await provider.snapshot() as { revision: { runtimeRevision: string; controlRevision: string | null } };
|
|
57
|
+
return { ...preview, ...(snapshot.revision.controlRevision ? { commandBinding: {
|
|
58
|
+
runtimeRevision: snapshot.revision.runtimeRevision, controlRevision: snapshot.revision.controlRevision } } : {}) };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
execute(sessionRef: string, command: unknown) {
|
|
62
|
+
return this.#serial(sessionRef, async () => {
|
|
63
|
+
const { info, provider, snapshot, binding } = await this.#live(sessionRef);
|
|
64
|
+
const controller = new SourceRevertController({ allowSessionReverts: true, projectRoot: info.cwd, bridge: { snapshot: binding },
|
|
65
|
+
evidence: this.#store.forSession(info.cwd, snapshot.identity.projectRef, snapshot.identity.sessionRef),
|
|
66
|
+
resolve: (file, hunks) => provider.sourceRevertAuthority(file, hunks),
|
|
67
|
+
revisions: async () => { provider.invalidate(); return (await provider.snapshot() as typeof snapshot).revision; },
|
|
68
|
+
mutate: request => piSourceMutationGuard.executeRevert({ cwd: info.cwd, rawSessionId: info.id, ...request }) });
|
|
69
|
+
const receipt = await controller.execute(command);
|
|
70
|
+
if (receipt.phase !== "rejected") { provider.invalidate(); this.#options.changed(sessionRef); }
|
|
71
|
+
return receipt;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|