@piagent/platform 1.48.4 → 1.48.5

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/docs/command-reference-vietnamese.md +2 -2
  3. package/docs/distribution-standard.md +6 -6
  4. package/docs/operator-manual-vietnamese.md +7 -7
  5. package/docs/publishing-for-teams.md +1 -0
  6. package/docs/release-install-policy.md +2 -2
  7. package/docs/team-onboarding.md +2 -2
  8. package/docs-site/architecture.html +1 -1
  9. package/docs-site/assets/search-en.json +1 -1
  10. package/docs-site/assets/search-vi.json +1 -1
  11. package/docs-site/benchmark.html +1 -1
  12. package/docs-site/commands.html +1 -1
  13. package/docs-site/context-engine.html +1 -1
  14. package/docs-site/ecosystem.html +1 -1
  15. package/docs-site/en/architecture.html +1 -1
  16. package/docs-site/en/benchmark.html +1 -1
  17. package/docs-site/en/commands.html +1 -1
  18. package/docs-site/en/context-engine.html +1 -1
  19. package/docs-site/en/ecosystem.html +1 -1
  20. package/docs-site/en/index.html +9 -9
  21. package/docs-site/en/mcp-approval.html +1 -1
  22. package/docs-site/en/mcp-auth.html +1 -1
  23. package/docs-site/en/mcp-servers.html +1 -1
  24. package/docs-site/en/mcp.html +1 -1
  25. package/docs-site/en/permissions.html +1 -1
  26. package/docs-site/en/profiles.html +1 -1
  27. package/docs-site/en/quickstart.html +1 -1
  28. package/docs-site/en/runtime.html +1 -1
  29. package/docs-site/en/scope.html +1 -1
  30. package/docs-site/en/security.html +1 -1
  31. package/docs-site/en/team.html +1 -1
  32. package/docs-site/en/update.html +3 -3
  33. package/docs-site/en/whats-new.html +25 -15
  34. package/docs-site/en/windows.html +1 -1
  35. package/docs-site/en/workflows.html +1 -1
  36. package/docs-site/index.html +9 -9
  37. package/docs-site/mcp-approval.html +1 -1
  38. package/docs-site/mcp-auth.html +1 -1
  39. package/docs-site/mcp-servers.html +1 -1
  40. package/docs-site/mcp.html +1 -1
  41. package/docs-site/permissions.html +1 -1
  42. package/docs-site/profiles.html +1 -1
  43. package/docs-site/quickstart.html +1 -1
  44. package/docs-site/releases.json +26 -0
  45. package/docs-site/runtime.html +1 -1
  46. package/docs-site/scope.html +1 -1
  47. package/docs-site/security.html +1 -1
  48. package/docs-site/team.html +1 -1
  49. package/docs-site/update.html +3 -3
  50. package/docs-site/whats-new.html +25 -15
  51. package/docs-site/windows.html +1 -1
  52. package/docs-site/workflows.html +1 -1
  53. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  54. package/package.json +1 -1
  55. package/packages/piagent-core/managed/review-snapshot-request.mjs +3 -1
  56. package/packages/piagent-core/managed/runtime-snapshot.mjs +63 -0
  57. package/packages/piagent-core/managed/terminal-boundary.mjs +4 -2
  58. package/packages/piagent-core/managed/tool-boundary.mjs +7 -7
  59. package/packages/piagent-core/managed/ts-diagnostics.mjs +1 -1
  60. package/packages/piagent-core/managed/workflow.mjs +2 -1
  61. package/packages/piagent-core/package.json +1 -1
@@ -0,0 +1,63 @@
1
+ // The code a company session's sandbox runs, kept apart from the install.
2
+ //
3
+ // A session pins the hash of its workers and refuses every tool when one
4
+ // changes. piagent-update rewrites the install in place, so a task running
5
+ // across an update lost all its tools (managed-runtime-changed) while a helper
6
+ // started afterwards worked. A session now runs its workers from a private
7
+ // copy named by their content: an update leaves it alone, the pins keep
8
+ // guarding it, and the next process takes a copy of the new release.
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { createHash, randomUUID } from 'node:crypto';
13
+
14
+ const KEEP_MS = 30 * 24 * 60 * 60 * 1000;
15
+ const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
16
+ const cacheRoot = home => path.join(home, process.platform === 'darwin' ? 'Library/Caches/Piagent/runtime' : '.cache/piagent/runtime');
17
+
18
+ function runtimeFiles(source) {
19
+ const files = fs.readdirSync(source, { withFileTypes: true }).filter(entry => entry.isFile() && entry.name.endsWith('.mjs')).map(entry => entry.name);
20
+ const walk = relative => {
21
+ let entries;
22
+ try { entries = fs.readdirSync(path.join(source, relative), { withFileTypes: true }); } catch { return; }
23
+ for (const entry of entries) {
24
+ const name = path.join(relative, entry.name);
25
+ if (entry.isDirectory()) walk(name); else if (entry.isFile()) files.push(name);
26
+ }
27
+ };
28
+ walk(path.join('dist', 'workspace'));
29
+ return files.sort();
30
+ }
31
+
32
+ const intact = (directory, listing) => listing.every(([file, digest]) => { try { return hash(path.join(directory, file)) === digest; } catch { return false; } });
33
+
34
+ // The copy of `source` under the member's cache folder; an old one is removed
35
+ // once no process has asked for it for a month.
36
+ export function snapshotRuntime(source, { home = os.homedir(), now = Date.now() } = {}) {
37
+ const listing = runtimeFiles(source).map(file => [file, hash(path.join(source, file))]);
38
+ const id = createHash('sha256').update(JSON.stringify(listing)).digest('hex').slice(0, 32);
39
+ const root = cacheRoot(home), target = path.join(root, id);
40
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
41
+ if (!intact(target, listing)) {
42
+ const building = path.join(root, `.${id}.${randomUUID()}`);
43
+ try {
44
+ for (const [file] of listing) {
45
+ fs.mkdirSync(path.dirname(path.join(building, file)), { recursive: true, mode: 0o700 });
46
+ fs.copyFileSync(path.join(source, file), path.join(building, file));
47
+ fs.chmodSync(path.join(building, file), 0o400);
48
+ }
49
+ // Without it the workspace worker's .js files would load as CommonJS.
50
+ fs.writeFileSync(path.join(building, 'package.json'), '{"type":"module"}\n', { mode: 0o400 });
51
+ fs.rmSync(target, { recursive: true, force: true });
52
+ // Another session may have built the same copy meanwhile.
53
+ try { fs.renameSync(building, target); } catch (error) { if (!intact(target, listing)) throw error; }
54
+ } finally { fs.rmSync(building, { recursive: true, force: true }); }
55
+ }
56
+ fs.utimesSync(target, new Date(now), new Date(now));
57
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
58
+ if (!entry.isDirectory() || entry.name === id) continue;
59
+ const old = path.join(root, entry.name);
60
+ try { if (now - fs.statSync(old).mtimeMs > KEEP_MS) fs.rmSync(old, { recursive: true, force: true }); } catch { /* another session removes it */ }
61
+ }
62
+ return fs.realpathSync(target);
63
+ }
@@ -9,18 +9,19 @@ import { companyTerminalRuntime } from './terminal-runtime.mjs';
9
9
  import { terminalChannel } from './terminal-channel.mjs';
10
10
  import { SEATBELT_TERMINAL_RULES, seatbeltTerminal } from './terminal-seatbelt.mjs';
11
11
 
12
- const worker = fileURLToPath(new URL('./terminal-worker.mjs', import.meta.url));
12
+ const installedWorker = fileURLToPath(new URL('./terminal-worker.mjs', import.meta.url));
13
13
  const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
14
14
  const inside = (file, root) => file === root || file.startsWith(root + path.sep);
15
15
  const dimensions = ({ cols, rows }) => Number.isInteger(cols) && cols >= 2 && cols <= 500 && Number.isInteger(rows) && rows >= 1 && rows <= 200;
16
16
 
17
17
  export class BoundaryTerminals {
18
- #boundary; #runtime; #workerDigest; #entries = new Map(); #closed = false;
18
+ #boundary; #runtime; #worker; #workerDigest; #entries = new Map(); #closed = false;
19
19
  reasonCode = null;
20
20
  constructor(boundary) {
21
21
  this.#boundary = boundary;
22
22
  try {
23
23
  this.#runtime = companyTerminalRuntime();
24
+ const worker = this.#worker = boundary.runtimeFile?.('terminal-worker.mjs') ?? installedWorker; // the session's own copy (runtime-snapshot.mjs)
24
25
  if (inside(this.#runtime.root, boundary.cwd) || inside(worker, boundary.cwd)) throw Error('managed-terminal-runtime-overlap');
25
26
  this.#workerDigest = hash(worker);
26
27
  } catch (error) { this.reasonCode = /^managed-terminal-/.test(error.message) ? error.message : 'managed-terminal-runtime-invalid'; }
@@ -33,6 +34,7 @@ export class BoundaryTerminals {
33
34
  if (!dimensions(size)) throw Error('managed-terminal-invalid-size');
34
35
  if (this.#entries.size >= 4) throw Error('managed-terminal-limit');
35
36
  this.#runtime.assertCurrent();
37
+ const worker = this.#worker;
36
38
  if (hash(worker) !== this.#workerDigest) throw Error('managed-terminal-runtime-changed');
37
39
  for (const [file, digest] of boundary.pins) if (hash(file) !== digest) throw Error('managed-runtime-changed');
38
40
  const temporary = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'piagent-terminal-')));
@@ -3,7 +3,7 @@ import { SessionMutationObservations } from "./session-mutation-observations.mjs
3
3
  import { BoundaryTerminals } from './terminal-boundary.mjs';
4
4
  import { ToolFreshness } from './tool-freshness.mjs';
5
5
  import { BoundaryQueue } from './boundary-queue.mjs';
6
- import { workspaceWorkerPins, verifyWorkspaceWorker } from './workspace-worker-pins.mjs';
6
+ import { workspaceWorkerPins, verifyWorkspaceWorker } from './workspace-worker-pins.mjs'; import { snapshotRuntime } from './runtime-snapshot.mjs';
7
7
  import fs from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
@@ -202,7 +202,7 @@ export class ManagedToolBoundary {
202
202
  get sourceControlRevision() { return this.#sourceRequests.revision; }
203
203
  source(request, binding) { return this.invoke("workspace", this.#sourceRequests.request(request, binding)); }
204
204
  #mutationObservations = new SessionMutationObservations();
205
- mutationObservation(file) { return this.#mutationObservations.get(file); }
205
+ mutationObservation(file) { return this.#mutationObservations.get(file); } runtimeFile(name) { return path.join(this.runtimeRoot, name); }
206
206
  #children = new Set();
207
207
  #closed = false;
208
208
  // readOnly: the project cannot be written. commands: bash is offered all
@@ -214,16 +214,16 @@ export class ManagedToolBoundary {
214
214
  if (process.platform !== 'darwin' && !(this.linux && BWRAP)) throw new Error('managed-sandbox-unavailable');
215
215
  this.cwd = fs.realpathSync(cwd); this.sdkRoot = fs.realpathSync(sdkRoot);
216
216
  this.node = fs.realpathSync(node);
217
- this.runtimeRoot = fs.realpathSync(path.dirname(workerPath));
217
+ this.sourceRoot = fs.realpathSync(path.dirname(workerPath)); this.runtimeRoot = snapshotRuntime(this.sourceRoot, { home: home ?? os.homedir() }); this.workerPath = this.runtimeFile('tool-worker.mjs'); // an update leaves a running session's workers alone
218
218
  this.workspacePins = workspaceWorkerPins(this.runtimeRoot);
219
219
  this.sdkPath = path.join(this.sdkRoot, 'dist/index.js');
220
220
  this.git = managedGit();
221
221
  if (JSON.parse(fs.readFileSync(path.join(this.sdkRoot, 'package.json'), 'utf8')).version !== PI_HOST_VERSION) throw new Error('managed-sdk-version-unqualified');
222
222
  const workerModules = ['review-snapshot-worker.mjs', 'tool-freshness.mjs', 'search-fallback.mjs', 'sandbox-diagnostic.mjs', 'ts-diagnostics-server.mjs', 'workspace-worker-pins.mjs'].map(name => path.join(this.runtimeRoot, name));
223
- this.pins = new Map([this.node, this.git, workerPath, ...workerModules, this.sdkPath].map(file => [file, hash(file)]));
223
+ this.pins = new Map([this.node, this.git, this.workerPath, ...workerModules, this.sdkPath].map(file => [file, hash(file)]));
224
224
  const userHome = fs.realpathSync(home ?? os.homedir());
225
- if ([userHome, '/', this.sdkRoot, this.runtimeRoot].includes(this.cwd)
226
- || [this.sdkRoot,this.runtimeRoot,this.node,this.git,...['/opt/homebrew/bin/gh','/usr/local/bin/gh'].filter(p=>fs.existsSync(p)).map(p=>fs.realpathSync(p))].some(p=>inside(p,this.cwd))) throw new Error('managed-project-overlaps-runtime');
225
+ if ([userHome, '/', this.sdkRoot, this.runtimeRoot, this.sourceRoot].includes(this.cwd)
226
+ || [this.sdkRoot,this.runtimeRoot,this.sourceRoot,this.node,this.git,...['/opt/homebrew/bin/gh','/usr/local/bin/gh'].filter(p=>fs.existsSync(p)).map(p=>fs.realpathSync(p))].some(p=>inside(p,this.cwd))) throw new Error('managed-project-overlaps-runtime');
227
227
  this.temporary = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'piagent-tools-')));
228
228
  fs.chmodSync(this.temporary, 0o700);
229
229
  this.home = path.join(this.temporary, 'home'); fs.mkdirSync(this.home, { mode: 0o700 });
@@ -371,7 +371,7 @@ export class ManagedToolBoundary {
371
371
  guard?.();
372
372
  const [sandbox, sandboxArgs] = this.linux ? [BWRAP, this.bubblewrap(network, docker)]
373
373
  : ['/usr/bin/sandbox-exec', ['-p', docker ? this.dockerProfile : network ? this.networkProfile : isolated ? this.isolatedProfile : this.profile]];
374
- const child = spawn(sandbox, [...sandboxArgs, this.node, workerPath, this.sdkPath, this.cwd], {
374
+ const child = spawn(sandbox, [...sandboxArgs, this.node, this.workerPath, this.sdkPath, this.cwd], {
375
375
  cwd: this.cwd, detached: true, stdio: ['pipe', 'pipe', 'pipe'],
376
376
  env: docker ? this.environment.docker : network ? this.environment.network : this.environment.offline,
377
377
  });
@@ -21,7 +21,7 @@ export class TsDiagnostics {
21
21
  if (this.#child) return this.#child;
22
22
  if (this.#starts >= RESTARTS) { this.#off = true; return null; }
23
23
  this.#starts += 1;
24
- const { child } = await this.boundary.spawnSandboxed([this.boundary.node, SERVER, this.boundary.cwd], randomUUID(), { stdin: 'pipe' });
24
+ const { child } = await this.boundary.spawnSandboxed([this.boundary.node, this.boundary.runtimeFile?.('ts-diagnostics-server.mjs') ?? SERVER, this.boundary.cwd], randomUUID(), { stdin: 'pipe' });
25
25
  child.stdout.on('data', chunk => {
26
26
  this.#buffer += chunk.toString('utf8');
27
27
  for (let at; (at = this.#buffer.indexOf('\n')) >= 0;) {
@@ -273,6 +273,7 @@ export class RunProcess {
273
273
  this.unplannedChange = true; this.planSkipped = true;
274
274
  }
275
275
  async afterTool(name, args, ok, digestOf) {
276
+ if (['write', 'edit'].includes(name) && ok) this.edited = true;
276
277
  if (name === 'bash' && isCheckCommand(args?.command, this.checks.commands)) {
277
278
  this.checksRun += 1; if (!ok) this.checksFailed += 1;
278
279
  this.lastCheck = { ok, digest: await digestOf(), mutations: this.mutations };
@@ -334,7 +335,7 @@ export async function completionGate(managed, run, signal) {
334
335
  let digest = null, interrupted = false, verifyGaveUp = false;
335
336
  for (let step = 0; step < 16; step += 1) {
336
337
  digest = await digestOf();
337
- run.changed = run.startDigest !== null && digest !== null ? digest !== run.startDigest : run.mutations > 0;
338
+ run.changed = run.startDigest !== null && digest !== null ? digest !== run.startDigest : run.startDigest !== null ? run.edited === true : run.mutations > 0; // code that cannot be read now: only a real edit says it changed
338
339
  // A check that passed still counts when the turn was cut off after it.
339
340
  if (run.changed) run.verified = run.verifiedOn(digest);
340
341
  run.docsOnly = run.changed && onlyNonCode(managed.ownPaths);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.48.4",
3
+ "version": "1.48.5",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",