@piagent/platform 1.37.1 → 1.38.2
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 +60 -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/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-DGWhP2MM.js +1 -0
- package/packages/piagent-webui/dist/client/assets/Button-QItfZUNj.js +1 -0
- package/packages/piagent-webui/dist/client/assets/DialogTitle-CyaFPaiW.js +3 -0
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-DtEwqme0.js +29 -0
- package/packages/piagent-webui/dist/client/assets/TerminalPanel-C3vkzjQ6.js +2 -0
- package/packages/piagent-webui/dist/client/assets/TerminalView-TNjhBGfa.js +9 -0
- package/packages/piagent-webui/dist/client/assets/TerminalView-kHJ-D0s7.css +1 -0
- package/packages/piagent-webui/dist/client/assets/Typography-B9om7Evq.js +1 -0
- package/packages/piagent-webui/dist/client/assets/WorkspaceCodeEditor-BAhYwQNM.js +2 -0
- package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-Coh_GWCO.js +5 -0
- package/packages/piagent-webui/dist/client/assets/WorkspaceQuickOpen-Bn4miZq4.js +1 -0
- package/packages/piagent-webui/dist/client/assets/bootstrap-CedkNtRG.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-B5MCHZv5.js +9 -0
- package/packages/piagent-webui/dist/client/assets/dist-BM_LL1o9.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-C2TMLUTo.js +23 -0
- package/packages/piagent-webui/dist/client/assets/dist-CqL3udNu.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-D4df0jLQ.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-D6i5SpQu.js +10 -0
- package/packages/piagent-webui/dist/client/assets/dist-DfjUS8LS.js +6 -0
- package/packages/piagent-webui/dist/client/assets/dist-IRUjhd1P.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-sNhoSseW.js +1 -0
- package/packages/piagent-webui/dist/client/assets/index-Brs_HjIo.css +1 -0
- package/packages/piagent-webui/dist/client/assets/index-a-perpOX.js +102 -0
- package/packages/piagent-webui/dist/client/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/packages/piagent-webui/dist/client/assets/tab-close-nAxrK7Lq.js +1 -0
- package/packages/piagent-webui/dist/client/assets/ui-preferences-BO3AV7QE.js +10 -0
- package/packages/piagent-webui/dist/client/assets/useId-DEefKUAT.js +124 -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 +15 -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,116 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { inspectWorkspaceEntry, readWorkspaceFile, WorkspaceReadError } from "../security/workspace-file-reader.ts";
|
|
5
|
+
import { workspacePathFlags, type WorkspacePolicy } from "./workspace-inspection.ts";
|
|
6
|
+
|
|
7
|
+
const MAX_BYTES = 1 << 20;
|
|
8
|
+
const digest = (bytes: Buffer) => createHash("sha256").update(bytes).digest("hex");
|
|
9
|
+
export class WorkspaceWriteError extends Error {
|
|
10
|
+
readonly code: "invalid-write" | "path-protected" | "read-only" | "stale-content" | "unsafe-path";
|
|
11
|
+
readonly currentDigest: string | null;
|
|
12
|
+
constructor(code: WorkspaceWriteError["code"], currentDigest: string | null = null) {
|
|
13
|
+
super(code); this.name = "WorkspaceWriteError"; this.code = code; this.currentDigest = currentDigest;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export type WorkspaceWriteInput = { path: string; content: string; expectedDigest: string; operation?: "write" | "rename" | "delete"; destination?: string };
|
|
17
|
+
|
|
18
|
+
const validPath = (value: unknown): value is string => typeof value === "string" && !!value && value.length <= 4096
|
|
19
|
+
&& !/[\\\x00-\x1f\x7f]/.test(value) && !path.isAbsolute(value) && !value.split("/").some(part => !part || part === "." || part === "..");
|
|
20
|
+
function fileState(root: string, relative: string): { digest: string; mode: number; stat: fs.Stats | null } {
|
|
21
|
+
try {
|
|
22
|
+
const stat = inspectWorkspaceEntry(root, relative);
|
|
23
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new WorkspaceWriteError("unsafe-path");
|
|
24
|
+
return { digest: digest(readWorkspaceFile(root, relative, MAX_BYTES)), mode: stat.mode & 0o777, stat };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// A new file gets the mode any editor would give it, not owner-only.
|
|
27
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { digest: "absent", mode: 0o666 & ~process.umask(), stat: null };
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function allowed(relative: string, policy: WorkspacePolicy) {
|
|
32
|
+
const flags = workspacePathFlags(relative, policy);
|
|
33
|
+
if (flags.protected) throw new WorkspaceWriteError("path-protected");
|
|
34
|
+
if (flags.readOnly || (policy.readOnly === undefined && policy.profile.permissionProfile === "read-only")) throw new WorkspaceWriteError("read-only");
|
|
35
|
+
}
|
|
36
|
+
// The folder a file lives in; a new file's missing folders are made, one
|
|
37
|
+
// checked segment at a time, never through a link.
|
|
38
|
+
function folder(root: string, relative: string, create: boolean, policy: WorkspacePolicy) {
|
|
39
|
+
const segments = path.posix.dirname(relative) === "." ? [] : path.posix.dirname(relative).split("/");
|
|
40
|
+
for (let index = 1; index <= segments.length; index++) {
|
|
41
|
+
const partial = segments.slice(0, index).join("/");
|
|
42
|
+
if (create) { allowed(partial, policy); try { fs.mkdirSync(path.join(root, partial)); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; } }
|
|
43
|
+
const stat = inspectWorkspaceEntry(root, partial);
|
|
44
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new WorkspaceWriteError("unsafe-path");
|
|
45
|
+
}
|
|
46
|
+
const parent = path.join(root, ...segments), stat = fs.lstatSync(parent);
|
|
47
|
+
if (!stat.isDirectory() || fs.realpathSync(parent) !== parent) throw new WorkspaceWriteError("unsafe-path");
|
|
48
|
+
return { parent, stat };
|
|
49
|
+
}
|
|
50
|
+
const unchanged = (parent: string, before: fs.Stats) => {
|
|
51
|
+
const now = fs.lstatSync(parent);
|
|
52
|
+
if (now.dev !== before.dev || now.ino !== before.ino || fs.realpathSync(parent) !== parent) throw new WorkspaceWriteError("unsafe-path");
|
|
53
|
+
};
|
|
54
|
+
// Errors that refuse before anything changed are named, so the member sees
|
|
55
|
+
// "refused" instead of "the result is uncertain".
|
|
56
|
+
function definite(error: unknown): never {
|
|
57
|
+
if (error instanceof WorkspaceWriteError) throw error;
|
|
58
|
+
if (error instanceof WorkspaceReadError) throw new WorkspaceWriteError(error.code === "oversized" ? "invalid-write" : error.code === "changed-during-read" ? "stale-content" : "unsafe-path");
|
|
59
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
60
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EEXIST") throw new WorkspaceWriteError("stale-content");
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Keep the final digest check and the rename/link synchronous, with no queued
|
|
65
|
+
// callback between them. Member writes never update the agent's observed read
|
|
66
|
+
// digest. A new file is linked into place, which fails rather than replace one
|
|
67
|
+
// that appeared meanwhile; rename and delete apply to a file whose content the
|
|
68
|
+
// member saw (its digest).
|
|
69
|
+
export function writeWorkspaceFile(rootPath: string, input: WorkspaceWriteInput, policy: WorkspacePolicy) {
|
|
70
|
+
const operation = input?.operation ?? "write";
|
|
71
|
+
if (!input || !validPath(input.path) || !["write", "rename", "delete"].includes(operation)
|
|
72
|
+
|| typeof input.content !== "string" || Buffer.byteLength(input.content) > MAX_BYTES
|
|
73
|
+
|| (input.expectedDigest !== "absent" && !/^[a-f0-9]{64}$/.test(input.expectedDigest))
|
|
74
|
+
|| (operation === "rename") !== (input.destination !== undefined) || operation === "rename" && (!validPath(input.destination) || input.destination === input.path)
|
|
75
|
+
|| operation !== "write" && (input.content !== "" || input.expectedDigest === "absent")) throw new WorkspaceWriteError("invalid-write");
|
|
76
|
+
allowed(input.path, policy);
|
|
77
|
+
if (operation === "rename") allowed(input.destination!, policy);
|
|
78
|
+
const root = fs.realpathSync(rootPath);
|
|
79
|
+
try {
|
|
80
|
+
const source = folder(root, input.path, operation === "write" && input.expectedDigest === "absent", policy);
|
|
81
|
+
const before = fileState(root, input.path);
|
|
82
|
+
if (before.digest !== input.expectedDigest) throw new WorkspaceWriteError("stale-content", before.digest);
|
|
83
|
+
const recheck = () => {
|
|
84
|
+
unchanged(source.parent, source.stat);
|
|
85
|
+
const current = fileState(root, input.path);
|
|
86
|
+
if (current.digest !== before.digest || current.stat?.ino !== before.stat?.ino || current.stat?.dev !== before.stat?.dev)
|
|
87
|
+
throw new WorkspaceWriteError("stale-content", current.digest);
|
|
88
|
+
};
|
|
89
|
+
if (operation === "delete") {
|
|
90
|
+
recheck(); fs.unlinkSync(path.join(root, input.path));
|
|
91
|
+
return { path: input.path, digest: before.digest, previousDigest: before.digest, created: false, revision: `sha256:${before.digest}` };
|
|
92
|
+
}
|
|
93
|
+
if (operation === "rename") {
|
|
94
|
+
const target = folder(root, input.destination!, true, policy);
|
|
95
|
+
if (fileState(root, input.destination!).digest !== "absent") throw new WorkspaceWriteError("stale-content");
|
|
96
|
+
recheck(); unchanged(target.parent, target.stat);
|
|
97
|
+
fs.linkSync(path.join(root, input.path), path.join(root, input.destination!));
|
|
98
|
+
fs.unlinkSync(path.join(root, input.path));
|
|
99
|
+
return { path: input.path, digest: before.digest, previousDigest: before.digest, created: false, revision: `sha256:${before.digest}` };
|
|
100
|
+
}
|
|
101
|
+
const bytes = Buffer.from(input.content), nextDigest = digest(bytes), target = path.join(root, input.path);
|
|
102
|
+
const temporary = path.join(source.parent, `.piagent-write-${process.pid}-${randomBytes(12).toString("hex")}`);
|
|
103
|
+
let descriptor: number | undefined;
|
|
104
|
+
try {
|
|
105
|
+
descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
106
|
+
fs.writeFileSync(descriptor, bytes); fs.fchmodSync(descriptor, before.mode); fs.fsyncSync(descriptor);
|
|
107
|
+
fs.closeSync(descriptor); descriptor = undefined;
|
|
108
|
+
recheck();
|
|
109
|
+
if (before.digest === "absent") fs.linkSync(temporary, target); else fs.renameSync(temporary, target);
|
|
110
|
+
return { path: input.path, digest: nextDigest, previousDigest: before.digest, created: before.digest === "absent", revision: `sha256:${nextDigest}` };
|
|
111
|
+
} finally {
|
|
112
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
113
|
+
fs.rmSync(temporary, { force: true });
|
|
114
|
+
}
|
|
115
|
+
} catch (error) { return definite(error); }
|
|
116
|
+
}
|
|
@@ -53,15 +53,20 @@ export class BackgroundProcesses {
|
|
|
53
53
|
entry.status = 'stopped'; entry.reason = reason; clearTimeout(entry.timer);
|
|
54
54
|
// Its process group, what it started (found by parent, since ps cannot
|
|
55
55
|
// read the environment of Apple's own binaries), then its marker.
|
|
56
|
-
|
|
57
|
-
for (const pid of [-entry.child.pid, ...tree]) try { process.kill(pid, 'SIGTERM'); } catch { /* already gone */ }
|
|
58
|
-
await this.boundary.stopBackground(entry.key);
|
|
59
|
-
for (const pid of tree) try { process.kill(pid, 'SIGKILL'); } catch { /* stopped */ }
|
|
56
|
+
await stopProcessGroup(entry.child, () => this.boundary.stopBackground(entry.key));
|
|
60
57
|
return entry;
|
|
61
58
|
}
|
|
62
59
|
async dispose() { await Promise.all(this.running().map(entry => this.stop(entry.id, 'the conversation closed'))); }
|
|
63
60
|
}
|
|
64
61
|
|
|
62
|
+
export async function stopProcessGroup(child, stopMarked = () => new Promise(resolve => setTimeout(resolve, 100))) {
|
|
63
|
+
if (!Number.isInteger(child.pid) || child.pid <= 1 || child.exitCode != null || child.signalCode != null) return;
|
|
64
|
+
const tree = descendants(child.pid);
|
|
65
|
+
for (const pid of [-child.pid, ...tree]) try { process.kill(pid, 'SIGTERM'); } catch { /* already gone */ }
|
|
66
|
+
await stopMarked();
|
|
67
|
+
for (const pid of [...tree, -child.pid]) try { process.kill(pid, 'SIGKILL'); } catch { /* stopped */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
function descendants(root) {
|
|
66
71
|
let rows = [];
|
|
67
72
|
try { rows = execFileSync('/bin/ps', ['-axo', 'pid=,ppid='], { encoding: 'utf8', timeout: 5000 }).trim().split('\n').map(row => row.trim().split(/\s+/).map(Number)); } catch { return []; }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { DEFAULT_DOMAINS, PackageProxy, mavenSettings, memberDomains } from './package-proxy.mjs';
|
|
6
|
+
|
|
7
|
+
export async function startBoundaryPackageProxy(boundary, apply) {
|
|
8
|
+
boundary.proxy = new PackageProxy({ domains: [...DEFAULT_DOMAINS, ...memberDomains(boundary.userHome)] });
|
|
9
|
+
try {
|
|
10
|
+
let url;
|
|
11
|
+
if (boundary.linux) {
|
|
12
|
+
boundary.proxySocket = path.join(os.tmpdir(), `piagent-proxy-${randomUUID().slice(0, 8)}.sock`);
|
|
13
|
+
await boundary.proxy.listen(boundary.proxySocket);
|
|
14
|
+
url = 'http://127.0.0.1:3128';
|
|
15
|
+
} else url = `http://127.0.0.1:${(await boundary.proxy.listen()).port}`;
|
|
16
|
+
const settings = path.join(boundary.home, '.m2/settings.xml');
|
|
17
|
+
fs.mkdirSync(path.dirname(settings), { recursive: true, mode: 0o700 });
|
|
18
|
+
fs.writeFileSync(settings, mavenSettings(url), { mode: 0o600 });
|
|
19
|
+
boundary.proxyUrl = url; apply();
|
|
20
|
+
} catch { await boundary.proxy.close().catch(() => {}); boundary.proxy = null; }
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// One queue spans the agent's reads and every change, the member's included,
|
|
2
|
+
// so two concurrent edits observe a deterministic order. What the member's
|
|
3
|
+
// explorer, editor and review panels only read does not wait behind the
|
|
4
|
+
// agent's running command: a few run beside it, a few more wait for them, and
|
|
5
|
+
// beyond that they are refused (the panels retry).
|
|
6
|
+
const LIMIT = 3, WAITING = 16;
|
|
7
|
+
const memberRead = (name, args) => name === 'workspace' && (['tree', 'file', 'paths', 'search', 'write-state'].includes(args?.kind)
|
|
8
|
+
|| args?.kind === 'source' && ['sources', 'diff', 'preview'].includes(args.request?.kind));
|
|
9
|
+
export class BoundaryQueue {
|
|
10
|
+
#tail = Promise.resolve();
|
|
11
|
+
#reads = 0;
|
|
12
|
+
#waiting = [];
|
|
13
|
+
run(name, args, action) {
|
|
14
|
+
if (!memberRead(name, args)) { const run = this.#tail.then(action); this.#tail = run.catch(() => {}); return run; }
|
|
15
|
+
if (this.#reads < LIMIT) return this.#read(action);
|
|
16
|
+
if (this.#waiting.length >= WAITING) return Promise.reject(new Error('managed-workspace-busy'));
|
|
17
|
+
return new Promise(resolve => this.#waiting.push(resolve)).then(() => this.#read(action));
|
|
18
|
+
}
|
|
19
|
+
#read(action) {
|
|
20
|
+
this.#reads++;
|
|
21
|
+
return Promise.resolve().then(action).finally(() => { this.#reads--; this.#waiting.shift()?.(); });
|
|
22
|
+
}
|
|
23
|
+
idle() { return this.#tail; }
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// This adapter contains no host Git/filesystem access. The pinned workspace
|
|
2
|
+
// worker applies path policy and byte-preserving Git operations in the sandbox.
|
|
3
|
+
export class BoundaryCheckpointGit {
|
|
4
|
+
constructor(managed) { this.managed = managed; }
|
|
5
|
+
request(operation, payload = {}, guard) {
|
|
6
|
+
guard?.();
|
|
7
|
+
return this.managed.boundary.invoke('workspace', { kind: 'checkpoint', operation, ...payload },
|
|
8
|
+
undefined, undefined, undefined, { guard });
|
|
9
|
+
}
|
|
10
|
+
available() { return this.request('available'); }
|
|
11
|
+
tree() { return this.request('tree'); }
|
|
12
|
+
changed(from, to) { return this.request('changed', { from, to }); }
|
|
13
|
+
restore(before, restore, remove, expectedTree, guard) {
|
|
14
|
+
return this.request('restore', { before, restore, remove, expectedTree }, guard);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,69 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// the run ends, the tree after it. /undo puts back the files the last turn
|
|
5
|
-
// changed, and refuses when one of them changed again since (the member's own
|
|
6
|
-
// edit is never overwritten). The member's index, HEAD and commits are not
|
|
7
|
-
// touched: a commit made in the turn stays. The private index is copied with
|
|
8
|
-
// its time kept, so git still re-reads a file changed within the same second
|
|
9
|
-
// at the same size. Commands run in the command sandbox.
|
|
10
|
-
const SNAPSHOT = `set -e; git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo NOREPO; exit 0; }
|
|
11
|
-
top=$(git rev-parse --show-toplevel); cd "$top"; idx="$TMPDIR/piagent-checkpoint-$$"
|
|
12
|
-
real=$(git rev-parse --git-path index); if [ -f "$real" ]; then cp -p "$real" "$idx"; fi
|
|
13
|
-
GIT_INDEX_FILE="$idx" git add -A >/dev/null 2>&1; tree=$(GIT_INDEX_FILE="$idx" git write-tree); rm -f "$idx"; echo "TREE $tree"`;
|
|
14
|
-
const b64 = list => Buffer.from(list.map(p => p + '\0').join(''), 'utf8').toString('base64');
|
|
1
|
+
import { BoundaryCheckpointGit } from './checkpoint-git.mjs';
|
|
2
|
+
import { TurnCheckpointService } from '../runtime/checkpoints/turn-checkpoints.mjs';
|
|
3
|
+
import { workspaceMemberEditsSince } from '../runtime/session/workspace-member-edits.mjs';
|
|
15
4
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
constructor(managed) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
// Keep the company API and sandbox execution while sharing checkpoint behavior.
|
|
6
|
+
export class TurnCheckpoints extends TurnCheckpointService {
|
|
7
|
+
constructor(managed) {
|
|
8
|
+
super(new BoundaryCheckpointGit(managed), { busy: () => managed.activePrompt,
|
|
9
|
+
memberEdits: since => workspaceMemberEditsSince(managed.session?.sessionManager, since) });
|
|
10
|
+
this.managed = managed;
|
|
22
11
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
run.checkpoint = null;
|
|
31
|
-
try { const before = await this.#tree(); if (before) { run.checkpoint = { before, after: null, at: Date.now() }; } } catch { /* no checkpoint: /undo says so */ }
|
|
32
|
-
}
|
|
33
|
-
// When the run ends: the tree after it, kept when the run changed something.
|
|
34
|
-
async finish(run = this.managed.run) {
|
|
35
|
-
const point = run?.checkpoint;
|
|
36
|
-
if (!point) return;
|
|
37
|
-
try { point.after = await this.#tree(); } catch { return; }
|
|
38
|
-
if (point.after && point.after !== point.before) this.#stack.push(point);
|
|
39
|
-
}
|
|
40
|
-
async #changed(from, to) {
|
|
41
|
-
const out = await this.#bash(`top=$(git rev-parse --show-toplevel) && cd "$top" && git diff --no-renames --name-status -z ${from} ${to}`);
|
|
42
|
-
const parts = out.split('\0').filter(Boolean), files = [];
|
|
43
|
-
for (let i = 0; i + 1 < parts.length; i += 2) files.push({ status: parts[i].trim(), path: parts[i + 1] });
|
|
44
|
-
return files;
|
|
45
|
-
}
|
|
46
|
-
// Puts back what the last turn changed: { result, restored, removed, files }
|
|
47
|
-
// for the WebUI note (phase "undo") and `text`, the sentence for the member.
|
|
48
|
-
async undo() {
|
|
49
|
-
const answer = (undo, text) => ({ ...undo, text });
|
|
50
|
-
if (this.managed.activePrompt) return answer({ result: 'busy', restored: 0, removed: 0, files: [] }, 'Đang có một lượt chạy: chờ nó xong (hoặc dừng nó) rồi /undo.');
|
|
51
|
-
const point = this.#stack.at(-1);
|
|
52
|
-
if (!point) return answer({ result: 'none', restored: 0, removed: 0, files: [] }, 'Không có lượt nào để hoàn tác trong cuộc trò chuyện này (chỉ áp dụng cho project là git repository; checkpoint có từ khi mở cuộc trò chuyện).');
|
|
53
|
-
const now = await this.#tree();
|
|
54
|
-
if (!now) return answer({ result: 'failed', restored: 0, removed: 0, files: [] }, 'Không đọc được trạng thái hiện tại của project; không hoàn tác.');
|
|
55
|
-
const turn = await this.#changed(point.before, point.after), since = new Set((await this.#changed(point.after, now)).map(f => f.path));
|
|
56
|
-
const touched = turn.filter(f => since.has(f.path)).map(f => f.path);
|
|
57
|
-
if (touched.length) return answer({ result: 'refused', restored: 0, removed: 0, files: touched.slice(0, 5) },
|
|
58
|
-
`Không hoàn tác: ${touched.length} file lượt trước đã sửa lại thay đổi sau đó (${touched.slice(0, 5).join(', ')}${touched.length > 5 ? ', …' : ''}). Hoàn tác sẽ ghi đè thay đổi đó.`);
|
|
59
|
-
const restore = turn.filter(f => f.status !== 'A').map(f => f.path), remove = turn.filter(f => f.status === 'A').map(f => f.path);
|
|
60
|
-
const steps = ['set -e', 'top=$(git rev-parse --show-toplevel); cd "$top"; idx="$TMPDIR/piagent-undo-$$"'];
|
|
61
|
-
if (restore.length) steps.push(`GIT_INDEX_FILE="$idx" git read-tree ${point.before}`, `printf %s ${b64(restore)} | base64 --decode | GIT_INDEX_FILE="$idx" git checkout-index -f -z --stdin`, 'rm -f "$idx"');
|
|
62
|
-
if (remove.length) steps.push(`printf %s ${b64(remove)} | base64 --decode | xargs -0 rm -f --`);
|
|
63
|
-
await this.#bash(steps.join('\n'));
|
|
64
|
-
this.#stack.pop();
|
|
65
|
-
return answer({ result: 'done', restored: restore.length, removed: remove.length, files: [...restore, ...remove].slice(0, 5), more: this.#stack.length > 0 },
|
|
66
|
-
`Đã hoàn tác lượt trước: ${restore.length} file trả về như trước lượt đó, ${remove.length} file lượt đó tạo đã xoá.${this.#stack.length ? ' /undo lần nữa để hoàn tác lượt trước nữa.' : ''} Commit tạo trong lượt đó (nếu có) vẫn giữ nguyên.`);
|
|
12
|
+
take(run = this.managed.run) {
|
|
13
|
+
if (run && !run.turnRef) {
|
|
14
|
+
const branch = this.managed.session?.sessionManager?.getBranch?.() ?? [];
|
|
15
|
+
const entry = [...branch].reverse().find(entry => entry.type === 'message' && entry.message?.role === 'user');
|
|
16
|
+
if (entry?.id) run.turnRef = entry.id;
|
|
17
|
+
}
|
|
18
|
+
return super.take(run);
|
|
67
19
|
}
|
|
20
|
+
finish(run = this.managed.run) { return super.finish(run); }
|
|
68
21
|
}
|
|
69
22
|
export const checkpointsOf = managed => (managed.checkpoints ??= new TurnCheckpoints(managed));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { networkConfirmation } from './permission.mjs';
|
|
2
|
+
import { asksNothing, broadAccess, networkConfirmation } from './permission.mjs';
|
|
3
3
|
import { engineAlive, engineLauncher } from './container-engine.mjs';
|
|
4
4
|
import { MEMBER_ONLY, memberOnlyReason, runHostCommand } from './host-command.mjs';
|
|
5
5
|
import { processTools } from './background-process.mjs';
|
|
@@ -34,9 +34,9 @@ const KINDS = {
|
|
|
34
34
|
description: 'Run one shell command on the member\'s machine, outside the sandbox, as the member, after they approve that exact command. Use it when the sandbox blocks what the task needs and no other tool covers it: install a runtime or tool the project needs (brew install openjdk@21, brew install node@22, sdk install java…), git pull or git push with the member\'s sign-in, a private registry, a file outside the project. It has the member\'s files, Keychain and sign-ins, so ask only for what the task needs and say why in reason. No input can be typed: commands that need the member\'s password (sudo, a .pkg installer) or an interactive sign-in are refused; ask the member with ask_user for those.',
|
|
35
35
|
allow: 'Run this exact command once on this machine, outside the sandbox, as you: it can read and change your files and use your sign-ins.',
|
|
36
36
|
title: 'Chạy lệnh trên máy (ngoài sandbox)', allowText: 'chạy đúng lệnh này một lần trên máy bạn, ngoài sandbox, bằng quyền của bạn: lệnh đọc/sửa được file và dùng được tài khoản đã đăng nhập trên máy.',
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
confirm: async (self, command) => await networkConfirmation(self, command) ?? 'ask',
|
|
37
|
+
// Full access does not cover it: the member approves each command (with
|
|
38
|
+
// what Full access would still have asked about). Bypass asks nothing.
|
|
39
|
+
confirm: async (self, command) => asksNothing(self) ? null : await networkConfirmation(self, command) ?? 'ask',
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
42
|
|
|
@@ -66,7 +66,7 @@ function commandTool(self, kind) {
|
|
|
66
66
|
if (memberOnly) throw Error(MEMBER_ONLY(memberOnly));
|
|
67
67
|
const confirm = await spec.confirm(self, args.command);
|
|
68
68
|
if (confirm !== null) await approve(self, id, signal, ctx, { toolName: spec.name, rawAction: { command: args.command }, commandPreview: String(args.command),
|
|
69
|
-
provider: spec.provider, requestedScope: spec.scope, reason: (confirm === 'ask' ? String(args.reason) : `
|
|
69
|
+
provider: spec.provider, requestedScope: spec.scope, reason: (confirm === 'ask' ? String(args.reason) : `Full access still asks: ${confirm}. ${args.reason}`).slice(0, 300), riskClass: spec.riskClass,
|
|
70
70
|
allowConsequence: spec.allow, denyConsequence: 'The command does not run; the agent is told you declined.' },
|
|
71
71
|
{ title: spec.title, body: `${args.command}\n\nLý do: ${args.reason}${confirm === 'ask' ? '' : `\nBypass vẫn hỏi: ${confirm}`}\n\nĐồng ý: ${spec.allowText}\nTừ chối: lệnh không chạy, agent được báo bạn đã từ chối.` });
|
|
72
72
|
let ok = false;
|
|
@@ -97,7 +97,7 @@ function startDockerTool(self, { wait = 180_000, every = 2000, launcher: found =
|
|
|
97
97
|
if (!launcher) throw Error(self.boundary.dockerCli ? MEMBER_ONLY("starting this machine's Docker service needs the member's password")
|
|
98
98
|
: 'managed-docker-not-installed: Máy chưa cài Docker. Hỏi member bằng ask_user: cài Docker Desktop hoặc OrbStack (trình cài cần mật khẩu nên member tự cài; máy có Homebrew thì có thể xin run_on_host "brew install colima docker"), hay chạy check theo cách khác.');
|
|
99
99
|
const preview = `${launcher.command} ${launcher.args.join(' ')}`;
|
|
100
|
-
if (self
|
|
100
|
+
if (!broadAccess(self)) await approve(self, id, signal, ctx, { toolName: 'start_docker', rawAction: { launcher: launcher.label }, commandPreview: preview,
|
|
101
101
|
provider: 'docker', requestedScope: 'start-docker-once', reason: String(args.reason).slice(0, 300), riskClass: 'medium',
|
|
102
102
|
allowConsequence: `Start ${launcher.label} on this machine; it keeps running after the conversation.`, denyConsequence: 'Docker is not started; the agent is told you declined.' },
|
|
103
103
|
{ title: `Mở ${launcher.label}`, body: `${preview}\n\nLý do: ${args.reason}\n\nĐồng ý: mở ${launcher.label} trên máy; nó vẫn chạy sau cuộc trò chuyện.\nTừ chối: không mở Docker, agent được báo bạn đã từ chối.` });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { checkpointsOf } from './checkpoints.mjs';
|
|
2
|
+
import { takeWorkspaceMemberEditNotice } from '../runtime/session/workspace-member-edits.mjs';
|
|
2
3
|
import { unfinished, planText, currentPlan, workflowPolicy } from './workflow.mjs';
|
|
3
4
|
import { failureCode } from '../runtime/managed-failure.mjs';
|
|
4
5
|
import { providerResumeMessage, resumeMessage } from './studio-link.mjs';
|
|
@@ -87,6 +88,8 @@ const fresh = (self, manager, text) => {
|
|
|
87
88
|
};
|
|
88
89
|
|
|
89
90
|
export async function memberTurn(self, { manager, prompt, runRound }, text, deliver) {
|
|
91
|
+
const notice = takeWorkspaceMemberEditNotice(manager);
|
|
92
|
+
if (notice) await self.session.sendCustomMessage(notice, { triggerTurn: false });
|
|
90
93
|
// /undo from the WebUI (the Terminal has its own command): the last turn's
|
|
91
94
|
// files go back, said in a note, with no model request (checkpoints.mjs).
|
|
92
95
|
if (/^\s*\/undo\s*$/.test(text)) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"projectId": "backend-api-project",
|
|
4
|
+
"displayName": "Backend API Project",
|
|
5
|
+
"mode": "backend-api",
|
|
6
|
+
"permissionProfile": "workspace-write",
|
|
7
|
+
"rootMarkers": [
|
|
8
|
+
"AGENTS.md",
|
|
9
|
+
"README.md",
|
|
10
|
+
"package.json",
|
|
11
|
+
"pom.xml",
|
|
12
|
+
"build.gradle",
|
|
13
|
+
"build.gradle.kts",
|
|
14
|
+
"pyproject.toml"
|
|
15
|
+
],
|
|
16
|
+
"protectedPaths": [
|
|
17
|
+
".git/**",
|
|
18
|
+
"**/auth.json",
|
|
19
|
+
"**/.env",
|
|
20
|
+
"**/.env.*",
|
|
21
|
+
"**/*.pem",
|
|
22
|
+
"**/*.key",
|
|
23
|
+
"**/node_modules/**",
|
|
24
|
+
"**/dist/**",
|
|
25
|
+
"**/build/**",
|
|
26
|
+
"**/target/**",
|
|
27
|
+
"**/.gradle/**",
|
|
28
|
+
"**/coverage/**"
|
|
29
|
+
],
|
|
30
|
+
"requiredContext": [
|
|
31
|
+
"AGENTS.md",
|
|
32
|
+
".pi/project-context.md",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"taskModes": {
|
|
36
|
+
"readOnly": {
|
|
37
|
+
"tools": [
|
|
38
|
+
"read",
|
|
39
|
+
"grep",
|
|
40
|
+
"find",
|
|
41
|
+
"ls"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"sourceWrite": {
|
|
45
|
+
"requiresPlan": true,
|
|
46
|
+
"requiresVerify": true
|
|
47
|
+
},
|
|
48
|
+
"migrationOrSchema": {
|
|
49
|
+
"requiresPlan": true,
|
|
50
|
+
"requiresVerify": true,
|
|
51
|
+
"requiresHumanGate": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"verifyCommands": {
|
|
55
|
+
"docsOnly": [
|
|
56
|
+
"test -s README.md"
|
|
57
|
+
],
|
|
58
|
+
"source": [
|
|
59
|
+
"if test -f package.json; then node -e \"const s=require('./package.json').scripts||{}; const n=['type-check','typecheck','lint','test']; if(!n.some(k=>s[k])){console.error('No backend Node verifier is configured');process.exit(2)}\" && npm run type-check --if-present && npm run typecheck --if-present && npm run lint --if-present && npm test --if-present; elif test -x ./mvnw; then ./mvnw test; elif test -x ./gradlew; then ./gradlew test; elif test -f pyproject.toml && command -v uv >/dev/null 2>&1; then uv run pytest; elif test -f pyproject.toml; then python -m pytest; else echo 'No backend verify command configured; update .pi/piagent-profile.json' >&2; exit 2; fi"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
"mcpCapabilities": [
|
|
63
|
+
"filesystem-readonly",
|
|
64
|
+
"filesystem-write",
|
|
65
|
+
"shell",
|
|
66
|
+
"github",
|
|
67
|
+
"database-readonly",
|
|
68
|
+
"memory"
|
|
69
|
+
],
|
|
70
|
+
"memory": {
|
|
71
|
+
"enabled": true,
|
|
72
|
+
"mode": "manual",
|
|
73
|
+
"scope": "project",
|
|
74
|
+
"summaryFile": ".pi/memory/memory_summary.md",
|
|
75
|
+
"handbookFile": ".pi/memory/MEMORY.md",
|
|
76
|
+
"localDir": ".pi/memory/local",
|
|
77
|
+
"readBeforeTask": true,
|
|
78
|
+
"writePolicy": "explicit-only",
|
|
79
|
+
"maxInjectedChars": 4000,
|
|
80
|
+
"externalPackages": []
|
|
81
|
+
},
|
|
82
|
+
"runtimePolicy": {
|
|
83
|
+
"execPolicy": "enforce",
|
|
84
|
+
"contextBudget": "enforce",
|
|
85
|
+
"toolRegistry": "advisory",
|
|
86
|
+
"finalGate": "enforce"
|
|
87
|
+
}
|
|
88
|
+
}
|
package/packages/piagent-core/managed/dist/workspace/assets/adapters/be-readonly-fe/profile.json
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"projectId": "be-readonly-fe-project",
|
|
4
|
+
"displayName": "Backend Readonly / Frontend Write Project",
|
|
5
|
+
"mode": "be-readonly-fe",
|
|
6
|
+
"permissionProfile": "workspace-write",
|
|
7
|
+
"rootMarkers": [
|
|
8
|
+
"AGENTS.md",
|
|
9
|
+
"README.md",
|
|
10
|
+
"package.json",
|
|
11
|
+
"frontend",
|
|
12
|
+
"backend",
|
|
13
|
+
"apps",
|
|
14
|
+
"packages"
|
|
15
|
+
],
|
|
16
|
+
"protectedPaths": [
|
|
17
|
+
".git/**",
|
|
18
|
+
"**/auth.json",
|
|
19
|
+
"**/.env",
|
|
20
|
+
"**/.env.*",
|
|
21
|
+
"**/*.pem",
|
|
22
|
+
"**/*.key",
|
|
23
|
+
"**/node_modules/**",
|
|
24
|
+
"**/dist/**",
|
|
25
|
+
"**/build/**",
|
|
26
|
+
"**/.next/**",
|
|
27
|
+
"**/coverage/**"
|
|
28
|
+
],
|
|
29
|
+
"readOnlyPaths": [
|
|
30
|
+
"backend/**",
|
|
31
|
+
"*backend*/**",
|
|
32
|
+
"server/**",
|
|
33
|
+
"api/**",
|
|
34
|
+
"apps/api/**",
|
|
35
|
+
"apps/server/**",
|
|
36
|
+
"apps/backend/**",
|
|
37
|
+
"packages/api/**",
|
|
38
|
+
"packages/server/**",
|
|
39
|
+
"packages/backend/**",
|
|
40
|
+
"services/*/**",
|
|
41
|
+
"**/migrations/**"
|
|
42
|
+
],
|
|
43
|
+
"shellProtectedPaths": [
|
|
44
|
+
".git/**",
|
|
45
|
+
"**/auth.json",
|
|
46
|
+
"**/.env",
|
|
47
|
+
"**/.env.*",
|
|
48
|
+
"**/*.pem",
|
|
49
|
+
"**/*.key",
|
|
50
|
+
"backend/**",
|
|
51
|
+
"*backend*/**",
|
|
52
|
+
"server/**",
|
|
53
|
+
"api/**",
|
|
54
|
+
"apps/api/**",
|
|
55
|
+
"apps/server/**",
|
|
56
|
+
"apps/backend/**",
|
|
57
|
+
"packages/api/**",
|
|
58
|
+
"packages/server/**",
|
|
59
|
+
"packages/backend/**",
|
|
60
|
+
"services/*/**",
|
|
61
|
+
"**/migrations/**",
|
|
62
|
+
"**/node_modules/**",
|
|
63
|
+
"**/dist/**",
|
|
64
|
+
"**/build/**",
|
|
65
|
+
"**/.next/**",
|
|
66
|
+
"**/coverage/**"
|
|
67
|
+
],
|
|
68
|
+
"requiredContext": [
|
|
69
|
+
"AGENTS.md",
|
|
70
|
+
".pi/project-context.md",
|
|
71
|
+
"README.md"
|
|
72
|
+
],
|
|
73
|
+
"taskModes": {
|
|
74
|
+
"backendScout": {
|
|
75
|
+
"readOnly": true,
|
|
76
|
+
"tools": [
|
|
77
|
+
"read",
|
|
78
|
+
"grep",
|
|
79
|
+
"find",
|
|
80
|
+
"ls"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"frontendSourceWrite": {
|
|
84
|
+
"requiresPlan": true,
|
|
85
|
+
"requiresContractSnapshot": true,
|
|
86
|
+
"requiresVerify": true
|
|
87
|
+
},
|
|
88
|
+
"backendContractGap": {
|
|
89
|
+
"requiresReport": true,
|
|
90
|
+
"backendWriteAllowed": false
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"verifyCommands": {
|
|
94
|
+
"docsOnly": [
|
|
95
|
+
"test -s README.md"
|
|
96
|
+
],
|
|
97
|
+
"frontendSource": [
|
|
98
|
+
"if test -f frontend/package.json; then cd frontend; elif test -f apps/web/package.json; then cd apps/web; elif test -f apps/frontend/package.json; then cd apps/frontend; elif test -f packages/web/package.json; then cd packages/web; elif test -f packages/frontend/package.json; then cd packages/frontend; elif test -f client/package.json; then cd client; elif test -f package.json; then :; else echo 'No frontend verify command configured; update .pi/piagent-profile.json' >&2; exit 2; fi; node -e \"const s=require('./package.json').scripts||{}; const n=['type-check','typecheck','lint','test']; if(!n.some(k=>s[k])){console.error('No frontend source verifier is configured');process.exit(2)}\" && npm run type-check --if-present && npm run typecheck --if-present && npm run lint --if-present && npm test --if-present"
|
|
99
|
+
],
|
|
100
|
+
"frontendRuntime": [
|
|
101
|
+
"if test -f frontend/package.json; then cd frontend; elif test -f apps/web/package.json; then cd apps/web; elif test -f apps/frontend/package.json; then cd apps/frontend; elif test -f packages/web/package.json; then cd packages/web; elif test -f packages/frontend/package.json; then cd packages/frontend; elif test -f client/package.json; then cd client; elif test -f package.json; then :; else echo 'No frontend runtime verify command configured; update .pi/piagent-profile.json' >&2; exit 2; fi; node -e \"const s=require('./package.json').scripts||{}; if(!s['test:e2e']){console.error('No frontend runtime verifier is configured');process.exit(2)}\" && npm run test:e2e"
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
"mcpCapabilities": [
|
|
105
|
+
"filesystem-readonly",
|
|
106
|
+
"filesystem-write",
|
|
107
|
+
"shell",
|
|
108
|
+
"github",
|
|
109
|
+
"browser",
|
|
110
|
+
"memory"
|
|
111
|
+
],
|
|
112
|
+
"memory": {
|
|
113
|
+
"enabled": true,
|
|
114
|
+
"mode": "manual",
|
|
115
|
+
"scope": "project",
|
|
116
|
+
"summaryFile": ".pi/memory/memory_summary.md",
|
|
117
|
+
"handbookFile": ".pi/memory/MEMORY.md",
|
|
118
|
+
"localDir": ".pi/memory/local",
|
|
119
|
+
"readBeforeTask": true,
|
|
120
|
+
"writePolicy": "explicit-only",
|
|
121
|
+
"maxInjectedChars": 4000,
|
|
122
|
+
"externalPackages": []
|
|
123
|
+
},
|
|
124
|
+
"runtimePolicy": {
|
|
125
|
+
"execPolicy": "enforce",
|
|
126
|
+
"contextBudget": "enforce",
|
|
127
|
+
"toolRegistry": "advisory",
|
|
128
|
+
"finalGate": "enforce"
|
|
129
|
+
}
|
|
130
|
+
}
|