@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
|
@@ -15,6 +15,7 @@ import { HELPER_CALLS, HELPER_SETUP, READ_TOOLS, countedCheck } from './helper-r
|
|
|
15
15
|
import { trackToolFailures } from './tool-failures.mjs';
|
|
16
16
|
import { helperMemoryTools, renewer } from './memory.mjs';
|
|
17
17
|
import { answeredSettlement, failedSettlement, settleHelper, OUTCOME_WORDS } from './helper-settlement.mjs';
|
|
18
|
+
import { viaLink } from './studio-link.mjs';
|
|
18
19
|
|
|
19
20
|
const textContent = result => result.content?.filter(p => p.type === 'text').map(p => p.text).join('\n') ?? '';
|
|
20
21
|
// A helper failure that another model of its role may not have: the model's
|
|
@@ -53,7 +54,16 @@ export async function runHelper(managed, role, task, signal, harness, { verifyGr
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
// A helper's role closes through the same wait as the run's close: a passing
|
|
58
|
+
// failure of Agent Watch's way to Studio is asked again. A close that still
|
|
59
|
+
// fails leaves the helper's answer standing: the role closes with the run,
|
|
60
|
+
// and the next helper of the role closes it first.
|
|
61
|
+
export async function closeRole(managed, role) {
|
|
62
|
+
try { await viaLink(managed.studioLink, () => managed.broker.request('close', { role })); managed.unclosed?.delete(role); }
|
|
63
|
+
catch { (managed.unclosed ??= new Set()).add(role); }
|
|
64
|
+
}
|
|
56
65
|
async function helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title }) {
|
|
66
|
+
if (managed.unclosed?.has(role)) await closeRole(managed, role);
|
|
57
67
|
// A long local tool step may outlive the main lease. Refresh authority
|
|
58
68
|
// before asking for a child; the broker serializes concurrent renewals,
|
|
59
69
|
// and main's own searches running beside it share managed renew.
|
|
@@ -143,6 +153,6 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
|
|
|
143
153
|
finally {
|
|
144
154
|
for (const session of sessions) session.dispose(); await boundary?.dispose();
|
|
145
155
|
await settleHelper(managed, renew.lease, report); // with the role's current token, before it closes
|
|
146
|
-
await managed
|
|
156
|
+
await closeRole(managed, role);
|
|
147
157
|
}
|
|
148
158
|
}
|
|
@@ -78,8 +78,11 @@ export function failedSettlement(role, error, signal, spec = {}) {
|
|
|
78
78
|
evidence: [], files: [], ...(Object.keys(spec ?? {}).length ? { spec } : {}) };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// Sends the report with the attempt's own token; never fails the helper.
|
|
82
|
-
|
|
81
|
+
// Sends the report with the attempt's own token; never fails the helper. A
|
|
82
|
+
// passing failure (no answer, 5xx, 429) is sent again after these pauses:
|
|
83
|
+
// Studio keeps one report per role, so a repeat is safe.
|
|
84
|
+
const SETTLE_PAUSES_MS = [500, 2_000];
|
|
85
|
+
export async function settleHelper(managed, grant, report, fetchImpl = (...args) => globalThis.fetch(...args), pauses = SETTLE_PAUSES_MS) {
|
|
83
86
|
if (!report || !hasCapability(managed, 'managed.settle.v1')) return false;
|
|
84
87
|
let held = null;
|
|
85
88
|
try {
|
|
@@ -87,10 +90,18 @@ export async function settleHelper(managed, grant, report, fetchImpl = (...args)
|
|
|
87
90
|
// current one (the attempt's first token may be stale by now).
|
|
88
91
|
if (typeof grant === 'function') { held = await grant(); grant = held.grant; }
|
|
89
92
|
if (!grant?.token) return false;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
94
|
+
let status = 0;
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetchImpl(`${managed.origin}/v1/managed/settle`, { method: 'POST', signal: AbortSignal.timeout(3_000),
|
|
97
|
+
headers: { Authorization: `Bearer ${grant.token}`, 'Content-Type': 'application/json', 'User-Agent': CLIENT_AGENT }, body: JSON.stringify(report) });
|
|
98
|
+
void response.body?.cancel?.().catch(() => {});
|
|
99
|
+
if (response.ok) return true;
|
|
100
|
+
status = response.status;
|
|
101
|
+
} catch { /* no answer */ }
|
|
102
|
+
if ((status && status < 500 && status !== 429) || attempt >= pauses.length) return false;
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, pauses[attempt]));
|
|
104
|
+
}
|
|
94
105
|
} catch { return false; } finally { held?.release(); }
|
|
95
106
|
}
|
|
96
107
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
|
|
3
|
+
// Whether an address is this machine, a private network or not routable on
|
|
4
|
+
// the internet (IANA special-purpose registries). One classifier for every
|
|
5
|
+
// outbound check (web-fetch, package-proxy). IPv6 forms that carry an IPv4
|
|
6
|
+
// address (mapped, compatible, translated, NAT64, 6to4), dotted or hex, are
|
|
7
|
+
// judged by that IPv4 address. Teredo, IPv6 outside 2000::/3, and anything
|
|
8
|
+
// that does not parse count as private: fail closed.
|
|
9
|
+
const V4_RANGES = [['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8], ['169.254.0.0', 16], ['172.16.0.0', 12],
|
|
10
|
+
['192.0.0.0', 24], ['192.0.2.0', 24], ['192.88.99.0', 24], ['192.168.0.0', 16], ['198.18.0.0', 15], ['198.51.100.0', 24],
|
|
11
|
+
['203.0.113.0', 24], ['224.0.0.0', 4], ['240.0.0.0', 4]].map(([base, bits]) => [v4Number(base), 2 ** (32 - bits)]);
|
|
12
|
+
|
|
13
|
+
function v4Number(text) {
|
|
14
|
+
const parts = String(text).split('.');
|
|
15
|
+
if (parts.length !== 4 || !parts.every(part => /^(0|[1-9]\d{0,2})$/.test(part) && Number(part) <= 255)) return null;
|
|
16
|
+
return parts.reduce((value, part) => value * 256 + Number(part), 0);
|
|
17
|
+
}
|
|
18
|
+
const v4Private = value => V4_RANGES.some(([base, size]) => value >= base && value < base + size);
|
|
19
|
+
|
|
20
|
+
// Eight 16-bit groups, or null: "::" expanded, a dotted IPv4 tail and a zone id accepted.
|
|
21
|
+
function v6Groups(text) {
|
|
22
|
+
let value = String(text).toLowerCase().replace(/%[^%]*$/, '');
|
|
23
|
+
const tail = /:(\d+\.\d+\.\d+\.\d+)$/.exec(value);
|
|
24
|
+
if (tail) {
|
|
25
|
+
const v4 = v4Number(tail[1]); if (v4 === null) return null;
|
|
26
|
+
value = value.slice(0, -tail[1].length) + `${(v4 >>> 16).toString(16)}:${(v4 & 0xffff).toString(16)}`;
|
|
27
|
+
}
|
|
28
|
+
const halves = value.split('::');
|
|
29
|
+
if (halves.length > 2) return null;
|
|
30
|
+
const head = halves[0] ? halves[0].split(':') : [], rest = halves[1] ? halves[1].split(':') : [];
|
|
31
|
+
const missing = 8 - head.length - rest.length;
|
|
32
|
+
if (halves.length === 2 ? missing < 1 : missing !== 0) return null;
|
|
33
|
+
const groups = [...head, ...Array(halves.length === 2 ? missing : 0).fill('0'), ...rest];
|
|
34
|
+
return groups.every(group => /^[0-9a-f]{1,4}$/.test(group)) ? groups.map(group => parseInt(group, 16)) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function privateAddress(address) {
|
|
38
|
+
if (net.isIPv4(String(address))) { const v4 = v4Number(address); return v4 === null || v4Private(v4); }
|
|
39
|
+
const g = v6Groups(address);
|
|
40
|
+
if (!g) return true;
|
|
41
|
+
const zero = (from, to) => g.slice(from, to).every(group => group === 0);
|
|
42
|
+
const v4 = (high, low) => v4Private(high * 0x10000 + low);
|
|
43
|
+
if (zero(0, 5) && (g[5] === 0xffff || g[5] === 0)) return v4(g[6], g[7]); // ::ffff:0:0/96 mapped, ::/96 compatible (with :: and ::1)
|
|
44
|
+
if (zero(0, 4) && g[4] === 0xffff && g[5] === 0) return v4(g[6], g[7]); // ::ffff:0:0:0/96 translated
|
|
45
|
+
if (g[0] === 0x64 && g[1] === 0xff9b) return zero(2, 6) ? v4(g[6], g[7]) : true; // NAT64 64:ff9b::/96; 64:ff9b:1::/48 local
|
|
46
|
+
if (g[0] === 0x2002) return v4(g[1], g[2]); // 6to4
|
|
47
|
+
if ((g[0] & 0xe000) !== 0x2000) return true; // not global unicast: 100::/64, fc00::/7, fe80::/10, fec0::/10, ff00::/8, ...
|
|
48
|
+
return g[0] === 0x2001 && (g[1] === 0 || g[1] === 0xdb8 || (g[1] === 2 && g[2] === 0)) // Teredo, documentation, benchmarking
|
|
49
|
+
|| (g[0] & 0xfff0) === 0x3ff0; // 3fff::/20 documentation
|
|
50
|
+
}
|
|
@@ -43,6 +43,9 @@ function passed(output) {
|
|
|
43
43
|
// What an error says, without what varies between runs: numbers, ids,
|
|
44
44
|
// temporary paths and quoted values.
|
|
45
45
|
export const errorShape = text => clean(text,180).replace(/\/(?:tmp|var\/folders|private)\/\S*/g,'<tmp>').replace(/\b[0-9a-f]{8,}\b/gi,'<id>').replace(/\d+/g,'N').replace(/(["'`])[^"'`]{1,80}\1/g,'$1…$1');
|
|
46
|
+
// A command without what varies between runs (temporary paths, ids,
|
|
47
|
+
// numbers): \`curl service-a\` and \`curl service-b\` stay two commands.
|
|
48
|
+
const commandShape = text => clean(text,160).replace(/\/(?:tmp|var\/folders|private)\/\S*/g,'<tmp>').replace(/\b[0-9a-f]{8,}\b/gi,'<id>').replace(/\d+/g,'N');
|
|
46
49
|
const ERROR_LINE = /error|fail|exception|not found|no such|denied|refused|cannot|unknown (?:option|flag|command)|invalid|EADDRINUSE|ENOENT|EACCES/i;
|
|
47
50
|
const MAX_LESSONS = 3;
|
|
48
51
|
|
|
@@ -55,7 +58,7 @@ export class CheckLessons {
|
|
|
55
58
|
if(/managed[-_](?:tool[-_]cancelled|operation[-_]denied|helper[-_]cancelled)|Command aborted|Command timed out/i.test(output))return;
|
|
56
59
|
const line=output.split('\n').find(l=>ERROR_LINE.test(l));
|
|
57
60
|
if(!line)return;
|
|
58
|
-
const command=
|
|
61
|
+
const command=commandShape(call.command);
|
|
59
62
|
if(!command)return;
|
|
60
63
|
const shape=errorShape(line), key=hash(command+'\0'+shape), seen=this.errors.get(key);
|
|
61
64
|
if(!seen){if(this.errors.size>=40)this.errors.delete(this.errors.keys().next().value);this.errors.set(key,{id:event.toolCallId,command:clean(call.command,160),error:clean(line,180),done:false});return;}
|
|
@@ -78,7 +78,7 @@ export class TeamMemory {
|
|
|
78
78
|
const scope = this.scope(), id = scopeID(scope);
|
|
79
79
|
if (id !== this.scopeHash) {
|
|
80
80
|
this.scopeHash = id; this.outbox = new MemoryOutbox(this.stateDir, scope, this.now);
|
|
81
|
-
this.protocol = 0; this.briefed = false; this.offUntil = 0; this.retain = true;
|
|
81
|
+
this.protocol = 0; this.briefed = false; this.offUntil = 0; this.retain = true; this.serves = new RoundServes(); // served under the old scope
|
|
82
82
|
}
|
|
83
83
|
return scope;
|
|
84
84
|
}
|
|
@@ -3,6 +3,9 @@ import http from 'node:http';
|
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { lookup } from 'node:dns/promises';
|
|
6
|
+
import { privateAddress } from './ip-policy.mjs';
|
|
7
|
+
|
|
8
|
+
export { privateAddress };
|
|
6
9
|
|
|
7
10
|
// The package registries a company command reaches without asking: plain
|
|
8
11
|
// bash has no network, except through this proxy, which the trusted runtime
|
|
@@ -10,6 +13,10 @@ import { lookup } from 'node:dns/promises';
|
|
|
10
13
|
// HTTP to the hosts below and the member's own additions, on ports 443 and
|
|
11
14
|
// 80 only, never to an IP literal or a name that resolves to this machine or
|
|
12
15
|
// a private network (DNS rebinding), and connects to the address it checked.
|
|
16
|
+
// With a company upstream proxy (HTTPS_PROXY) the name is still looked up and
|
|
17
|
+
// checked here, and the upstream is asked for that checked address, never the
|
|
18
|
+
// name, so it cannot resolve it elsewhere; a name this machine cannot look up
|
|
19
|
+
// is refused (run_with_network still reaches it after the member approves).
|
|
13
20
|
// Everything else is refused with 403 and remembered, so the command's
|
|
14
21
|
// failure names the host and run_with_network (the member approves it).
|
|
15
22
|
// Registries (and GitHub) take no upload without a token, and tokens stay
|
|
@@ -50,17 +57,8 @@ export function domainAllowed(host, domains) {
|
|
|
50
57
|
return domains.some(rule => rule.startsWith('*.') ? name.endsWith(rule.slice(1)) && name.length > rule.length - 1 : name === rule);
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
if (net.isIPv4(address)) {
|
|
56
|
-
const [a, b] = address.split('.').map(Number);
|
|
57
|
-
return a === 0 || a === 10 || a === 127 || (a === 100 && b >= 64 && b < 128) || (a === 169 && b === 254) || (a === 172 && b >= 16 && b < 32)
|
|
58
|
-
|| (a === 192 && b === 168) || (a === 198 && (b === 18 || b === 19)) || a >= 224;
|
|
59
|
-
}
|
|
60
|
-
const v6 = address.toLowerCase();
|
|
61
|
-
if (v6.startsWith('::ffff:')) return privateAddress(v6.slice(7));
|
|
62
|
-
return v6 === '::' || v6 === '::1' || /^f[cd]/.test(v6) || /^fe[89ab]/.test(v6) || v6.startsWith('ff');
|
|
63
|
-
}
|
|
60
|
+
// host:port of a checked address, as a CONNECT target or URL host.
|
|
61
|
+
const authority = ({ host, port }) => `${net.isIPv6(host) ? `[${host}]` : host}:${port}`;
|
|
64
62
|
|
|
65
63
|
function upstreamProxy(env) {
|
|
66
64
|
for (const name of ['HTTPS_PROXY', 'https_proxy']) {
|
|
@@ -101,7 +99,6 @@ export class PackageProxy {
|
|
|
101
99
|
// The checked public address of an allowed host, or null.
|
|
102
100
|
async #target(host, port) {
|
|
103
101
|
if (!this.ports.has(port) || !domainAllowed(host, this.domains)) { this.#deny(host); return null; }
|
|
104
|
-
if (this.upstream) return { host, port };
|
|
105
102
|
try {
|
|
106
103
|
const addresses = await this.resolve(host);
|
|
107
104
|
if (!addresses.length || addresses.some(entry => this.isPrivate(entry.address))) { this.#deny(host); return null; }
|
|
@@ -117,7 +114,7 @@ export class PackageProxy {
|
|
|
117
114
|
upstream.setTimeout(10 * 60_000, () => upstream.destroy());
|
|
118
115
|
upstream.once('connect', () => {
|
|
119
116
|
if (this.upstream) {
|
|
120
|
-
upstream.write(`CONNECT ${
|
|
117
|
+
upstream.write(`CONNECT ${authority(target)} HTTP/1.1\r\nHost: ${authority(target)}\r\n\r\n`);
|
|
121
118
|
let reply = Buffer.alloc(0);
|
|
122
119
|
const onData = chunk => {
|
|
123
120
|
reply = Buffer.concat([reply, chunk]);
|
|
@@ -144,8 +141,9 @@ export class PackageProxy {
|
|
|
144
141
|
const target = await this.#target(url.hostname, Number(url.port) || 80);
|
|
145
142
|
if (!target) { response.writeHead(403, { 'X-Piagent-Proxy': 'domain-not-allowed', 'Content-Type': 'text/plain; charset=utf-8' }).end(`piagent: ${url.hostname} is not an allowed package registry\n`); return; }
|
|
146
143
|
const headers = { ...request.headers, host: url.host }; delete headers['proxy-connection']; delete headers['proxy-authorization'];
|
|
144
|
+
const pinned = new URL(url); pinned.host = authority(target);
|
|
147
145
|
const outgoing = http.request(this.upstream
|
|
148
|
-
? { host: this.upstream.host, port: this.upstream.port, path:
|
|
146
|
+
? { host: this.upstream.host, port: this.upstream.port, path: pinned.href, method: request.method, headers }
|
|
149
147
|
: { host: target.host, port: target.port, path: url.pathname + url.search, method: request.method, headers, servername: url.hostname }, incoming => {
|
|
150
148
|
response.writeHead(incoming.statusCode ?? 502, incoming.headers); incoming.pipe(response);
|
|
151
149
|
});
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { invokeReviewSnapshot } from './review-snapshot-request.mjs';
|
|
5
5
|
|
|
6
|
-
const quote = value => "'" + value.replaceAll("'", "'\"'\"'") + "'";
|
|
7
6
|
const textContent = result => result.content?.filter(p => p.type === 'text').map(p => p.text).join('\n') ?? '';
|
|
8
|
-
const WORKER = fileURLToPath(new URL('./review-snapshot-worker.mjs', import.meta.url));
|
|
9
7
|
const MAX_SNAPSHOT = 16 * 1024 * 1024;
|
|
10
8
|
|
|
11
9
|
// The worker runs inside the tool boundary; what it prints is one short line.
|
|
@@ -20,8 +18,8 @@ async function worker(boundary, mode, scope = null, base = null) {
|
|
|
20
18
|
fs.writeFileSync(path.join(boundary.temporary, scopeName), JSON.stringify([...scope]), { mode: 0o600, flag: 'wx' });
|
|
21
19
|
}
|
|
22
20
|
let result;
|
|
23
|
-
const
|
|
24
|
-
try { result = await boundary
|
|
21
|
+
const extra = [...(scopeName ? [scopeName] : []), ...(typeof base === 'string' && BASE.test(base) ? [base] : [])];
|
|
22
|
+
try { result = await invokeReviewSnapshot(boundary, mode, extra); }
|
|
25
23
|
finally { if (scopeName) fs.rmSync(path.join(boundary.temporary, scopeName), { force: true }); }
|
|
26
24
|
const text = textContent(result).trim();
|
|
27
25
|
if (result.details?.truncation?.truncated || text.includes('\n')) throw Error(/review-(patch|file|untracked)-limit/.exec(text)?.[0] ?? 'managed-review-patch-unreadable');
|
|
@@ -37,7 +35,7 @@ export async function readPatchDigest(boundary, scope = null, base = null) { ret
|
|
|
37
35
|
// The commit HEAD names now, or null (no repository, no commit yet).
|
|
38
36
|
export async function readHead(boundary) {
|
|
39
37
|
try {
|
|
40
|
-
const result = await boundary
|
|
38
|
+
const result = await invokeReviewSnapshot(boundary, 'head');
|
|
41
39
|
const text = textContent(result).trim();
|
|
42
40
|
return BASE.test(text) ? text : null;
|
|
43
41
|
} catch { return null; }
|
|
@@ -48,7 +46,7 @@ export async function readHead(boundary) {
|
|
|
48
46
|
export async function resolveReviewBase(boundary, ref) {
|
|
49
47
|
if (typeof ref !== 'string' || !/^[A-Za-z0-9._\/~^@{}-]{1,200}$/.test(ref) || ref.startsWith('-')) return null;
|
|
50
48
|
try {
|
|
51
|
-
const result = await boundary
|
|
49
|
+
const result = await invokeReviewSnapshot(boundary, 'since', [ref]);
|
|
52
50
|
const text = textContent(result).trim();
|
|
53
51
|
return BASE.test(text) ? text : null;
|
|
54
52
|
} catch { return null; }
|
|
@@ -57,7 +55,7 @@ export async function resolveReviewBase(boundary, ref) {
|
|
|
57
55
|
// came since: { base, ref }, or null.
|
|
58
56
|
export async function readBranchBase(boundary) {
|
|
59
57
|
try {
|
|
60
|
-
const result = await boundary
|
|
58
|
+
const result = await invokeReviewSnapshot(boundary, 'branch-base');
|
|
61
59
|
const [base, ref] = textContent(result).trim().split(' ');
|
|
62
60
|
return BASE.test(base ?? '') && ref ? { base, ref } : null;
|
|
63
61
|
} catch { return null; }
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
// A company conversation's access, chosen by the member and kept with the
|
|
2
2
|
// conversation: "workspace-write" asks before every command that needs the
|
|
3
|
-
// internet; "trusted-full-access" (
|
|
4
|
-
// confirmed (runtime/policy/bypass-policy.mjs)
|
|
3
|
+
// internet; "trusted-full-access" (Full access) asks only for what must be
|
|
4
|
+
// confirmed (runtime/policy/bypass-policy.mjs); "bypass" asks nothing, as
|
|
5
|
+
// Codex's bypass mode, where the team's Harness allows it
|
|
6
|
+
// (harness.configuration.bypass_mode, an administrator's switch in Studio).
|
|
7
|
+
// The sandbox itself stays: bypass removes the questions, not the boundary.
|
|
5
8
|
import { mustConfirm } from '../runtime/policy/bypass-policy.mjs';
|
|
6
9
|
|
|
7
|
-
export const PERMISSIONS = ['workspace-write', 'trusted-full-access'];
|
|
10
|
+
export const PERMISSIONS = ['workspace-write', 'trusted-full-access', 'bypass'];
|
|
8
11
|
const ENTRY = 'agent-watch-permission';
|
|
9
12
|
|
|
13
|
+
// Only an explicit yes from Studio: an Agent Watch that does not carry the
|
|
14
|
+
// field, or a Harness an administrator switched off, gives no bypass.
|
|
15
|
+
export const bypassAllowed = manifest => manifest?.harness?.configuration?.bypass_mode === true;
|
|
16
|
+
// What applies now: a conversation left in bypass after the administrator
|
|
17
|
+
// switched it off falls back to Full access.
|
|
18
|
+
export const effectivePermission = owner => owner.permission === 'bypass' && !bypassAllowed(owner.manifest) ? 'trusted-full-access' : owner.permission;
|
|
19
|
+
|
|
10
20
|
export function restorePermission(manager) {
|
|
11
21
|
const chosen = manager.getEntries().filter(e => e.type === 'custom' && e.customType === ENTRY).at(-1)?.data?.mode;
|
|
12
22
|
return PERMISSIONS.includes(chosen) ? chosen : 'workspace-write';
|
|
@@ -14,9 +24,22 @@ export function restorePermission(manager) {
|
|
|
14
24
|
export function permissionSetter(owner, manager) {
|
|
15
25
|
return mode => {
|
|
16
26
|
if (!PERMISSIONS.includes(mode)) throw Error('managed-permission-unavailable');
|
|
27
|
+
if (mode === 'bypass' && !bypassAllowed(owner.manifest)) throw Error('managed-bypass-unavailable');
|
|
17
28
|
manager.appendCustomEntry(ENTRY, { mode, at: new Date().toISOString() }); owner.permission = mode;
|
|
18
29
|
};
|
|
19
30
|
}
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
31
|
+
// The conversation records whether its Harness allows Bypass, so the
|
|
32
|
+
// dashboard offers it (or not) without the runtime.
|
|
33
|
+
const ALLOWANCE = 'agent-watch-bypass';
|
|
34
|
+
export function recordBypass(owner, manager) {
|
|
35
|
+
const allowed = bypassAllowed(owner.manifest);
|
|
36
|
+
const last = manager.getEntries().filter(e => e.type === 'custom' && e.customType === ALLOWANCE).at(-1)?.data;
|
|
37
|
+
if ((last?.allowed === true) !== allowed) manager.appendCustomEntry(ALLOWANCE, { allowed, at: new Date().toISOString() });
|
|
38
|
+
}
|
|
39
|
+
// Whether this conversation may skip the questions Full access still asks.
|
|
40
|
+
export const asksNothing = owner => effectivePermission(owner) === 'bypass';
|
|
41
|
+
export const broadAccess = owner => ['trusted-full-access', 'bypass'].includes(effectivePermission(owner));
|
|
42
|
+
// Why a network command must still be asked about: null runs it in Full
|
|
43
|
+
// access or Bypass, 'ask' when the member asks first.
|
|
44
|
+
export const networkConfirmation = async (owner, command) => asksNothing(owner) ? null
|
|
45
|
+
: effectivePermission(owner) === 'trusted-full-access' ? mustConfirm(command) : 'ask';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { broadAccess } from './permission.mjs';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -112,7 +113,7 @@ export function fetchOriginTool(self) {
|
|
|
112
113
|
parameters:{type:'object',properties:{path:{type:'string',maxLength:500,description:'Repository folder inside the conversation folder, such as "api"; omit when the conversation folder is the repository.'}},additionalProperties:false},
|
|
113
114
|
execute:async(id,args,signal,_update,ctx)=>{
|
|
114
115
|
const plan=await repositoryFetchPlan(self.boundary,args?.path);
|
|
115
|
-
if(self
|
|
116
|
+
if(broadAccess(self)&&!signal?.aborted)return executeRepositoryFetch(self.boundary,plan,signal);
|
|
116
117
|
const {piApprovalBroker}=await import('../runtime/inspection/approval-broker.ts');
|
|
117
118
|
const decision=await piApprovalBroker.request({cwd:self.cwd,rawSessionId:self.session.sessionManager.getSessionId(),toolCallId:id,
|
|
118
119
|
action:{kind:'external-provider-action',preconditionClass:'runtime-only',toolName:'fetch_origin',rawAction:plan,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
|
|
3
|
+
const worker = fileURLToPath(new URL('./review-snapshot-worker.mjs', import.meta.url));
|
|
4
|
+
const requests = new WeakSet();
|
|
5
|
+
const quote = value => "'" + value.replaceAll("'", "'\"'\"'") + "'";
|
|
6
|
+
|
|
7
|
+
// Only fixed runtime snapshot reads preserve provenance. JSON tool arguments
|
|
8
|
+
// cannot forge this identity; freezing prevents substitution before dispatch.
|
|
9
|
+
export function invokeReviewSnapshot(boundary, mode, extra = []) {
|
|
10
|
+
if (!['head', 'since', 'branch-base', 'digest', 'file', 'files'].includes(mode)
|
|
11
|
+
|| !Array.isArray(extra) || extra.some(value => typeof value !== 'string')) throw Error('review-request-invalid');
|
|
12
|
+
const args = Object.freeze({ command: [process.execPath, worker, mode, ...extra].map(quote).join(' '), timeout: 60 });
|
|
13
|
+
requests.add(args);
|
|
14
|
+
return boundary.invoke('bash', args);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function consumeReviewSnapshot(args) {
|
|
18
|
+
const trusted = requests.has(args);
|
|
19
|
+
requests.delete(args);
|
|
20
|
+
return trusted;
|
|
21
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { companyTerminalDiagnostic } from './terminal-runtime.mjs';
|
|
2
|
+
|
|
1
3
|
// What a failed command tells the agent. Every environment failure names the
|
|
2
4
|
// way on (owner, 2026-10-13): a tool that asks the member's approval
|
|
3
5
|
// (run_with_network, run_with_docker, start_docker, fetch_origin, run_on_host)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { freshnessPath } from './tool-freshness.mjs';
|
|
3
|
+
|
|
4
|
+
// Trusted sandbox metadata only. No project filesystem access in the parent.
|
|
5
|
+
// Every consumer must revalidate policy and digests inside the boundary.
|
|
6
|
+
export class SessionMutationObservations {
|
|
7
|
+
#sessionId = randomUUID();
|
|
8
|
+
#files = new Map();
|
|
9
|
+
get(file) { return structuredClone(this.#files.get(file) ?? null); }
|
|
10
|
+
snapshot() { return structuredClone([...this.#files.entries()]); }
|
|
11
|
+
clear() { this.#files.clear(); }
|
|
12
|
+
prepare(cwd, name, args, enabled) {
|
|
13
|
+
const file = freshnessPath(cwd, name, args);
|
|
14
|
+
const request = enabled ? { sessionId: this.#sessionId, toolCallId: randomUUID(), prior: this.get(file) } : undefined;
|
|
15
|
+
if (['edit', 'write'].includes(name)) this.#files.delete(file);
|
|
16
|
+
if (name === 'bash') this.clear();
|
|
17
|
+
if (name === 'workspace' && args?.kind === 'write') this.#files.delete(args.payload?.path);
|
|
18
|
+
return request;
|
|
19
|
+
}
|
|
20
|
+
acceptRevert(args, trusted, result, update) {
|
|
21
|
+
if (args?.kind !== 'source' || args.request?.kind !== 'execute') return;
|
|
22
|
+
const file = args.request.target?.path, prior = this.#files.get(file);
|
|
23
|
+
this.#files.delete(file);
|
|
24
|
+
const next = update?.observation;
|
|
25
|
+
if (!trusted || result?.state !== 'settled' || update?.path !== file || !prior || !next || next.path !== file
|
|
26
|
+
|| next.toolCallId !== prior.toolCallId || JSON.stringify(next.baseline) !== JSON.stringify(prior.baseline)
|
|
27
|
+
|| !/^[a-f0-9]{64}$/.test(next.currentDigest) || !/^session-mutation\.[a-f0-9]{64}$/.test(next.evidenceRef)) return;
|
|
28
|
+
this.#files.set(file, structuredClone(next));
|
|
29
|
+
}
|
|
30
|
+
accept(cwd, name, args, request, observation) {
|
|
31
|
+
const file = freshnessPath(cwd, name, args);
|
|
32
|
+
if (!request || !file || observation?.path !== file || observation.toolCallId !== request.toolCallId
|
|
33
|
+
|| typeof observation.baseline?.content !== 'string' || Buffer.byteLength(observation.baseline.content) > 256 * 1024) return;
|
|
34
|
+
this.#files.set(file, observation);
|
|
35
|
+
while (this.#files.size > 32) this.#files.delete(this.#files.keys().next().value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -13,7 +13,7 @@ import { managedThinkingLevels, nearestLevel } from './capabilities.mjs';
|
|
|
13
13
|
import { nativeManagedModel, isVendor, piProvider, studioAPI, studioPath } from './native-catalog.mjs';
|
|
14
14
|
import { fetchOriginTool } from './repository-operation.mjs';
|
|
15
15
|
import { describeFailure, failureCode, failureText } from '../runtime/managed-failure.mjs';
|
|
16
|
-
import { enableCompanyTurnRetry, wrapRoleStreams, conversationSession } from './request-stream.mjs'; import { StudioLink,
|
|
16
|
+
import { enableCompanyTurnRetry, wrapRoleStreams, conversationSession } from './request-stream.mjs'; import { StudioLink, closeRun } from './studio-link.mjs';
|
|
17
17
|
import { trackToolFailures } from './tool-failures.mjs';
|
|
18
18
|
import { fastServed, fastSetter, fastState, recordFast, restoreFast } from './fast-mode.mjs';
|
|
19
19
|
import { runHelper } from './helper-run.mjs';
|
|
@@ -26,7 +26,7 @@ import { workflowPolicy, repositoryChecks, planTool, currentPlan, PLAN_ENTRY, wo
|
|
|
26
26
|
import { beforeDelegate } from './objections.mjs';
|
|
27
27
|
import { HELPER_CALLS, HELPER_ROLES, helperRoles, helperPrompt, webPrompt, delegateDescription } from './helper-roles.mjs'; import { delegateParameters, delegateArgs, SPEC_PROMPT } from './helper-settlement.mjs';
|
|
28
28
|
import { loadAgentResources } from './agent-skills.mjs';
|
|
29
|
-
import { restorePermission, permissionSetter } from './permission.mjs';
|
|
29
|
+
import { restorePermission, recordBypass, permissionSetter } from './permission.mjs';
|
|
30
30
|
import { commandTools, servicesPrompt } from './command-tools.mjs'; import { attachMemory, memoryToolsFor, openRound, closeRound, prepareMemoryContext, trackMemoryLessons } from './memory.mjs';
|
|
31
31
|
|
|
32
32
|
const PROVIDER = 'agent_watch_managed';
|
|
@@ -169,7 +169,7 @@ export class ManagedSession {
|
|
|
169
169
|
self.session.managedExecution = true;
|
|
170
170
|
self.turnRetry = enableCompanyTurnRetry(self.session, settings, self.studioLink);
|
|
171
171
|
// The member's access for this conversation: ask first, or Bypass (permission.mjs).
|
|
172
|
-
self.permission = restorePermission(manager); self.session.managedSetPermission = permissionSetter(self, manager);
|
|
172
|
+
self.permission = restorePermission(manager); recordBypass(self, manager); self.session.managedSetPermission = permissionSetter(self, manager);
|
|
173
173
|
// Fast mode, where the Harness allows it (fast-mode.mjs): off until the member turns it on.
|
|
174
174
|
self.fast = restoreFast(manager); recordFast(self, manager); self.session.managedSetFast = fastSetter(self, manager); self.session.managedFastState = () => fastState(self); // null where the Harness does not allow it
|
|
175
175
|
self.session.managedFastServed = () => { const route = self.routes.get(self.modelRuntime); return route ? fastServed(route) : null; };
|
|
@@ -296,15 +296,14 @@ export class ManagedSession {
|
|
|
296
296
|
// the newest version the broker advertises.
|
|
297
297
|
const features = self.manifest.broker_features ?? [];
|
|
298
298
|
const process = self.run && features.includes('process') ? self.run.report(currentPlan(manager), features.includes('process-v3') ? 3 : features.includes('process-v2') ? 2 : 1) : null;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
manager.appendCustomEntry('agent-watch-run', { run_id: self.grant.run_id, state: 'closed' });
|
|
299
|
+
// A close Studio may not have had stays "active" here (closeRun).
|
|
300
|
+
if (await closeRun(self.studioLink, self.broker, features, process, { conversation: conversationSession(self), runId: self.grant.run_id })) manager.appendCustomEntry('agent-watch-run', { run_id: self.grant.run_id, state: 'closed' });
|
|
302
301
|
}
|
|
303
302
|
// A run that could not be closed (the network or Agent Watch went away)
|
|
304
303
|
// ends the turn as failed; the run stays "active" here, so reopening
|
|
305
304
|
// the conversation recovers or abandons it.
|
|
306
305
|
} catch (error) { round.closeFailed = failureCode(failureText(error)) || 'close_failed'; }
|
|
307
|
-
finally { self.grant = null; self.run = null; self.preflight = null; }
|
|
306
|
+
finally { self.grant = null; self.run = null; self.preflight = null; self.unclosed = null; }
|
|
308
307
|
}
|
|
309
308
|
};
|
|
310
309
|
self.session.prompt = async (text, options) => {
|
|
@@ -5,9 +5,17 @@
|
|
|
5
5
|
// Piagent never edits a skill itself and makes no model call for this.
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
8
9
|
|
|
9
10
|
const MIN_FAILURES = 3, MIN_SHARE = 0.5, KEEP = 10, REPROPOSE_MS = 7 * 86400_000;
|
|
10
|
-
|
|
11
|
+
// One file name per skill name: a name with other characters (or capitals,
|
|
12
|
+
// on a case-insensitive disk) gets a digest of itself, so \`a/b\` and \`a_b\`
|
|
13
|
+
// keep their own counts.
|
|
14
|
+
const safeName = name => {
|
|
15
|
+
name = String(name);
|
|
16
|
+
if (/^[a-z0-9_.-]{1,80}$/.test(name) && !/^\.+$/.test(name)) return name;
|
|
17
|
+
return `${name.replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 60)}~${createHash('sha256').update(name).digest('hex').slice(0, 12)}`;
|
|
18
|
+
};
|
|
11
19
|
|
|
12
20
|
export class SkillFeedback {
|
|
13
21
|
// A managed round's tracker, when the conversation has skills.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// Authorization lives outside JSON arguments. Model tools cannot manufacture
|
|
4
|
+
// the object identity needed to obtain the boundary's key or observations.
|
|
5
|
+
export class SourceRevertRequests {
|
|
6
|
+
#key = randomBytes(32).toString('hex');
|
|
7
|
+
#requests = new WeakMap();
|
|
8
|
+
get revision() { return `source-control.${createHash("sha256").update(this.#key).digest("hex")}`; }
|
|
9
|
+
request(request, binding) {
|
|
10
|
+
const args = { kind: 'source', request: structuredClone(request) };
|
|
11
|
+
this.#requests.set(args, binding);
|
|
12
|
+
return args;
|
|
13
|
+
}
|
|
14
|
+
consume(args, observations) {
|
|
15
|
+
if (args?.kind !== 'source') return undefined;
|
|
16
|
+
const binding = this.#requests.get(args); this.#requests.delete(args);
|
|
17
|
+
if (!binding || !binding.current() || !binding.idle()) throw new Error('source-session-unavailable');
|
|
18
|
+
return { identity: structuredClone(binding.identity), rawSessionId: binding.rawSessionId,
|
|
19
|
+
controlRevision: binding.controlRevision, confirmationKey: this.#key, observations: observations.snapshot() };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -269,3 +269,19 @@ export async function viaLink(link, ask, { conversation = null, runId = null } =
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
+
|
|
273
|
+
// Closing a round's run, asked again through the link. Only a refused report
|
|
274
|
+
// closes again without it; an unconfirmed close throws (the turn fails). An
|
|
275
|
+
// Agent Watch without close-retry forgets a run whose close failed, so asking
|
|
276
|
+
// again "succeeds" without reaching Studio: false then, and the run stays
|
|
277
|
+
// "active" for the runtime (reopening recovers or abandons it).
|
|
278
|
+
export async function closeRun(link, broker, features, process, where) {
|
|
279
|
+
let unsent = false;
|
|
280
|
+
const close = args => viaLink(link, () => broker.request('close', args).catch(error => {
|
|
281
|
+
if (!features.includes('close-retry') && String(error?.message) !== 'managed-broker:invalidResponse') unsent = true;
|
|
282
|
+
throw error;
|
|
283
|
+
}), where);
|
|
284
|
+
try { await close(process ? { process } : {}); }
|
|
285
|
+
catch (error) { if (!process || String(error?.message) !== 'managed-broker:invalidResponse') throw error; await close(); }
|
|
286
|
+
return !unsent;
|
|
287
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { BWRAP, bubblewrapArgs, credentialFiles } from './linux-sandbox.mjs';
|
|
8
|
+
import { companyTerminalRuntime } from './terminal-runtime.mjs';
|
|
9
|
+
import { terminalChannel } from './terminal-channel.mjs';
|
|
10
|
+
import { SEATBELT_TERMINAL_RULES, seatbeltTerminal } from './terminal-seatbelt.mjs';
|
|
11
|
+
|
|
12
|
+
const worker = fileURLToPath(new URL('./terminal-worker.mjs', import.meta.url));
|
|
13
|
+
const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
14
|
+
const inside = (file, root) => file === root || file.startsWith(root + path.sep);
|
|
15
|
+
const dimensions = ({ cols, rows }) => Number.isInteger(cols) && cols >= 2 && cols <= 500 && Number.isInteger(rows) && rows >= 1 && rows <= 200;
|
|
16
|
+
|
|
17
|
+
export class BoundaryTerminals {
|
|
18
|
+
#boundary; #runtime; #workerDigest; #entries = new Map(); #closed = false;
|
|
19
|
+
reasonCode = null;
|
|
20
|
+
constructor(boundary) {
|
|
21
|
+
this.#boundary = boundary;
|
|
22
|
+
try {
|
|
23
|
+
this.#runtime = companyTerminalRuntime();
|
|
24
|
+
if (inside(this.#runtime.root, boundary.cwd) || inside(worker, boundary.cwd)) throw Error('managed-terminal-runtime-overlap');
|
|
25
|
+
this.#workerDigest = hash(worker);
|
|
26
|
+
} catch (error) { this.reasonCode = /^managed-terminal-/.test(error.message) ? error.message : 'managed-terminal-runtime-invalid'; }
|
|
27
|
+
}
|
|
28
|
+
async open(size, emit) {
|
|
29
|
+
if (this.#closed) throw Error('managed-terminal-boundary-closed');
|
|
30
|
+
if (this.reasonCode) throw Error(this.reasonCode);
|
|
31
|
+
const boundary = this.#boundary;
|
|
32
|
+
if (!boundary.allowed.includes('bash')) throw Error('managed-terminal-permission-denied');
|
|
33
|
+
if (!dimensions(size)) throw Error('managed-terminal-invalid-size');
|
|
34
|
+
if (this.#entries.size >= 4) throw Error('managed-terminal-limit');
|
|
35
|
+
this.#runtime.assertCurrent();
|
|
36
|
+
if (hash(worker) !== this.#workerDigest) throw Error('managed-terminal-runtime-changed');
|
|
37
|
+
for (const [file, digest] of boundary.pins) if (hash(file) !== digest) throw Error('managed-runtime-changed');
|
|
38
|
+
const temporary = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'piagent-terminal-')));
|
|
39
|
+
fs.chmodSync(temporary, 0o700);
|
|
40
|
+
let channel;
|
|
41
|
+
try {
|
|
42
|
+
const home = path.join(temporary, 'home'), empty = path.join(temporary, 'empty');
|
|
43
|
+
fs.mkdirSync(home, { mode: 0o700 }); fs.writeFileSync(empty, '', { mode: 0o400 });
|
|
44
|
+
const env = Object.fromEntries(Object.entries(boundary.environment.offline)
|
|
45
|
+
.filter(([key]) => !/proxy/i.test(key) && !['NODE_OPTIONS', 'NODE_PATH', 'BASH_ENV', 'ENV', 'PIAGENT_TOOL_SCOPE'].includes(key))
|
|
46
|
+
.map(([key, value]) => [key, String(value).split(boundary.temporary).join(temporary)]));
|
|
47
|
+
Object.assign(env, { HOME: home, TMPDIR: temporary, TERM: 'xterm-256color', BASH_SILENCE_DEPRECATION_WARNING: '1',
|
|
48
|
+
PIAGENT_PROCESS_SCOPE: `${boundary.scope}:${randomUUID()}` });
|
|
49
|
+
// A separate temporary root excludes the boundary's package-proxy socket.
|
|
50
|
+
// Writes reach the project, its Git directories and the project's
|
|
51
|
+
// package caches, as the agent's own offline commands do.
|
|
52
|
+
if (!boundary.linux) channel = seatbeltTerminal({ entry: this.#runtime.entry, cwd: boundary.cwd, env, size, emit,
|
|
53
|
+
profile: boundary.terminalProfile(temporary) + SEATBELT_TERMINAL_RULES });
|
|
54
|
+
else {
|
|
55
|
+
const base = boundary.linuxBase;
|
|
56
|
+
const protectedPaths = (base.denied ?? []).filter(file => fs.existsSync(file))
|
|
57
|
+
.map(file => ({ path: file, directory: fs.statSync(file).isDirectory() }));
|
|
58
|
+
const args = bubblewrapArgs({ ...base, temporary, empty, network: false, proxySocket: null, docker: null,
|
|
59
|
+
toolchains: [...base.toolchains, this.#runtime.root],
|
|
60
|
+
credentials: [...credentialFiles([boundary.cwd, ...base.repository, ...base.references]), ...protectedPaths] });
|
|
61
|
+
// There is intentionally no alternative spawn path or injected host shell.
|
|
62
|
+
const child = spawn(BWRAP, [...args, boundary.node, worker, this.#runtime.entry,
|
|
63
|
+
fs.readlinkSync('/proc/self/ns/pid'), fs.readlinkSync('/proc/self/ns/net'), String(size.cols), String(size.rows)],
|
|
64
|
+
{ cwd: boundary.cwd, detached: true, stdio: ['pipe', 'pipe', 'pipe'], env });
|
|
65
|
+
channel = terminalChannel(child, emit);
|
|
66
|
+
}
|
|
67
|
+
const id = randomUUID();
|
|
68
|
+
this.#entries.set(id, channel);
|
|
69
|
+
void channel.completed.then(() => { this.#entries.delete(id); fs.rmSync(temporary, { recursive: true, force: true }); }).catch(() => {});
|
|
70
|
+
await channel.initialized;
|
|
71
|
+
return { id, input(data) {
|
|
72
|
+
if (typeof data !== 'string' || Buffer.byteLength(data) > 16384) return Promise.reject(Error('managed-terminal-input-limit'));
|
|
73
|
+
return channel.execute({ kind: 'input', data });
|
|
74
|
+
}, resize(size) {
|
|
75
|
+
if (!dimensions(size)) return Promise.reject(Error('managed-terminal-invalid-size'));
|
|
76
|
+
return channel.execute({ kind: 'resize', cols: size.cols, rows: size.rows });
|
|
77
|
+
}, close: channel.close };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (channel) await channel.close();
|
|
80
|
+
else fs.rmSync(temporary, { recursive: true, force: true });
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async dispose() {
|
|
85
|
+
this.#closed = true;
|
|
86
|
+
await Promise.all([...this.#entries.values()].map(channel => channel.close()));
|
|
87
|
+
}
|
|
88
|
+
}
|