@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.
- package/CHANGELOG.md +9 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +1 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/assets/search-en.json +1 -1
- package/docs-site/assets/search-vi.json +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- 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 +9 -9
- 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/update.html +3 -3
- package/docs-site/en/whats-new.html +25 -15
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +9 -9
- 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/releases.json +26 -0
- 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/update.html +3 -3
- package/docs-site/whats-new.html +25 -15
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/managed/review-snapshot-request.mjs +3 -1
- package/packages/piagent-core/managed/runtime-snapshot.mjs +63 -0
- package/packages/piagent-core/managed/terminal-boundary.mjs +4 -2
- package/packages/piagent-core/managed/tool-boundary.mjs +7 -7
- package/packages/piagent-core/managed/ts-diagnostics.mjs +1 -1
- package/packages/piagent-core/managed/workflow.mjs +2 -1
- 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
|
|
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.
|
|
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);
|