@piagent/platform 1.7.1 → 1.9.0
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/AGENTS.md +7 -0
- package/CHANGELOG.md +113 -1
- package/README.md +45 -70
- package/README.vi.md +34 -61
- package/architecture/layers.json +16 -3
- package/catalog/capabilities.json +14 -7
- package/catalog/claude-model-additions.json +42 -0
- package/docs/auto-delegation-policy.md +2 -0
- package/docs/command-reference-vietnamese.md +69 -91
- package/docs/context-window-policy.md +5 -5
- package/docs/distribution-standard.md +16 -19
- package/docs/en/architecture.md +9 -1
- package/docs/herdr-workflow.md +2 -3
- package/docs/languages.json +4 -4
- package/docs/managed-local-acceptance.md +403 -0
- package/docs/mcp-and-tools.md +5 -7
- package/docs/memory-policy.md +3 -3
- package/docs/model-options.md +22 -17
- package/docs/oauth-providers.md +1 -1
- package/docs/operator-manual-vietnamese.md +85 -130
- package/docs/package-architecture-notes.md +4 -4
- package/docs/piagent-core-improvement-plan.md +3 -5
- package/docs/piagent-core-plan-checkpoint-2026-09-06.json +2 -13
- package/docs/piagent-runtime-benchmark-execution-plan.md +10 -8
- package/docs/project-adapters.md +3 -3
- package/docs/project-onboarding.md +15 -16
- package/docs/publishing-for-teams.md +3 -1
- package/docs/quality-benchmark.md +2 -0
- package/docs/quickstart-vietnamese.md +24 -25
- package/docs/release-install-policy.md +16 -11
- package/docs/research/harness-next-evidence-review.md +7 -7
- package/docs/runtime-harness-standard.md +2 -0
- package/docs/runtime-policy-design.md +2 -0
- package/docs/runtime-quality-baseline.md +2 -0
- package/docs/subagent-orchestration-capabilities.md +1 -1
- package/docs/subagents-and-multiagent.md +10 -8
- package/docs/task-implementation-contract.md +2 -0
- package/docs/task-lifecycle-vietnamese.md +2 -0
- package/docs/team-onboarding.md +33 -64
- package/docs/usage-observability.md +2 -2
- package/docs/vi/architecture.md +9 -1
- package/docs/workflow-recipes.md +6 -10
- package/docs-site/architecture.html +4 -3
- package/docs-site/assets/docs.css +80 -0
- package/docs-site/benchmark.html +7 -1
- package/docs-site/commands.html +37 -23
- package/docs-site/content/architecture.html +2 -2
- package/docs-site/content/benchmark.html +5 -0
- package/docs-site/content/commands.html +35 -22
- package/docs-site/content/ecosystem.html +144 -0
- package/docs-site/content/en/architecture.html +1 -1
- package/docs-site/content/en/benchmark.html +5 -0
- package/docs-site/content/en/commands.html +30 -13
- package/docs-site/content/en/ecosystem.html +144 -0
- package/docs-site/content/en/index.html +24 -9
- package/docs-site/content/en/quickstart.html +10 -12
- package/docs-site/content/en/runtime.html +1 -1
- package/docs-site/content/en/security.html +1 -1
- package/docs-site/content/en/team.html +2 -2
- package/docs-site/content/en/whats-new.html +89 -1
- package/docs-site/content/en/workflows.html +29 -31
- package/docs-site/content/index.html +24 -9
- package/docs-site/content/quickstart.html +10 -12
- package/docs-site/content/runtime.html +2 -2
- package/docs-site/content/security.html +2 -2
- package/docs-site/content/team.html +2 -2
- package/docs-site/content/whats-new.html +89 -1
- package/docs-site/content/workflows.html +36 -38
- package/docs-site/context-engine.html +2 -1
- package/docs-site/ecosystem.html +283 -0
- package/docs-site/en/architecture.html +3 -2
- package/docs-site/en/benchmark.html +7 -1
- package/docs-site/en/commands.html +32 -14
- package/docs-site/en/context-engine.html +2 -1
- package/docs-site/en/ecosystem.html +283 -0
- package/docs-site/en/index.html +30 -14
- package/docs-site/en/mcp-approval.html +2 -1
- package/docs-site/en/mcp-auth.html +2 -1
- package/docs-site/en/mcp-servers.html +2 -1
- package/docs-site/en/mcp.html +2 -1
- package/docs-site/en/permissions.html +2 -1
- package/docs-site/en/profiles.html +2 -1
- package/docs-site/en/quickstart.html +17 -18
- package/docs-site/en/runtime.html +3 -2
- package/docs-site/en/scope.html +2 -1
- package/docs-site/en/security.html +6 -5
- package/docs-site/en/team.html +4 -3
- package/docs-site/en/whats-new.html +97 -8
- package/docs-site/en/workflows.html +34 -35
- package/docs-site/index.html +30 -14
- package/docs-site/mcp-approval.html +2 -1
- package/docs-site/mcp-auth.html +2 -1
- package/docs-site/mcp-servers.html +2 -1
- package/docs-site/mcp.html +2 -1
- package/docs-site/permissions.html +2 -1
- package/docs-site/profiles.html +2 -1
- package/docs-site/quickstart.html +17 -18
- package/docs-site/runtime.html +4 -3
- package/docs-site/scope.html +2 -1
- package/docs-site/security.html +7 -6
- package/docs-site/team.html +4 -3
- package/docs-site/whats-new.html +97 -8
- package/docs-site/workflows.html +41 -42
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/private-holdout-v1/CURRENT_BOUNDARY.md +17 -0
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +10 -9
- package/packages/piagent-core/README.md +15 -23
- package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +2 -0
- package/packages/piagent-core/extensions/experiment-loop-policy.ts +75 -0
- package/packages/piagent-core/extensions/local-state-path.js +15 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +108 -960
- package/packages/piagent-core/extensions/policy-core.js +2 -2
- package/packages/piagent-core/extensions/source-checkout-paths.ts +54 -0
- package/packages/piagent-core/managed/broker-client.mjs +49 -0
- package/packages/piagent-core/managed/capabilities.mjs +28 -0
- package/packages/piagent-core/managed/compaction.mjs +63 -0
- package/packages/piagent-core/managed/doctor.mjs +107 -0
- package/packages/piagent-core/managed/native-catalog.mjs +42 -0
- package/packages/piagent-core/managed/patch-snapshot.mjs +48 -0
- package/packages/piagent-core/managed/repository-operation.mjs +84 -0
- package/packages/piagent-core/managed/request-stream.mjs +129 -0
- package/packages/piagent-core/managed/resource-loader.mjs +42 -0
- package/packages/piagent-core/managed/review-snapshot-worker.mjs +27 -0
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +12 -0
- package/packages/piagent-core/managed/search-fallback.mjs +93 -0
- package/packages/piagent-core/managed/session.mjs +472 -0
- package/packages/piagent-core/managed/store.d.mts +3 -0
- package/packages/piagent-core/managed/store.mjs +64 -0
- package/packages/piagent-core/managed/tool-boundary.mjs +243 -0
- package/packages/piagent-core/managed/tool-worker.mjs +36 -0
- package/packages/piagent-core/managed/toolchain.mjs +120 -0
- package/packages/piagent-core/managed/web-fetch.mjs +105 -0
- package/packages/piagent-core/managed/web-search.mjs +96 -0
- package/packages/piagent-core/managed/workflow.mjs +299 -0
- package/packages/piagent-core/package.json +3 -6
- package/packages/piagent-core/policies/base-policy.json +80 -26
- package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +3 -3
- package/packages/piagent-core/runtime/hooks/input-hook.ts +35 -196
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -0
- package/packages/piagent-core/runtime/inspection/approval-broker.ts +9 -1
- package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +1 -1
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +1 -1
- package/packages/piagent-core/runtime/managed-failure.d.mts +10 -0
- package/packages/piagent-core/runtime/managed-failure.mjs +164 -0
- package/packages/piagent-core/runtime/model/model-route-policy.ts +33 -13
- package/packages/piagent-core/runtime/model/model-route-types.ts +10 -6
- package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +8 -29
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +1 -1
- package/packages/piagent-core/runtime/policy/block-remedy.ts +5 -3
- package/packages/piagent-core/runtime/registration/context-commands.ts +1 -1
- package/packages/piagent-core/runtime/registration/knowledge-tools.ts +7 -7
- package/packages/piagent-core/runtime/registration/policy-tools.ts +2 -2
- package/packages/piagent-core/runtime/registration/session-commands.ts +11 -26
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
- package/packages/piagent-core/runtime/session/system-prompt.ts +6 -7
- package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +6 -6
- package/packages/piagent-core/runtime/workflows/webui-workflow.ts +6 -11
- package/packages/piagent-core/security/sensitive-text.js +9 -0
- package/packages/piagent-core/skills/piagent-ops/SKILL.md +8 -10
- package/packages/piagent-core/skills/test-audit/SKILL.md +101 -0
- package/packages/piagent-core/subagents/piagent-planner.md +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DGk34zJT.js → MarkdownMessage-Dd9wPk-v.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-7I-50Q2U.js +228 -0
- package/packages/piagent-webui/dist/client/assets/{index-DVahUVWy.css → index-DgSmcnP8.css} +1 -1
- package/packages/piagent-webui/dist/client/index.html +2 -2
- package/packages/piagent-webui/extension/piagent-webui.ts +11 -11
- package/packages/piagent-webui/extension/session-stream-adapter.ts +3 -1
- package/packages/piagent-webui/extension/terminal-session-adapter.ts +6 -1
- package/packages/piagent-webui/gateway/company-relay.ts +308 -0
- package/packages/piagent-webui/gateway/control-socket.ts +39 -12
- package/packages/piagent-webui/gateway/gateway-service.ts +109 -28
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +58 -2
- package/packages/piagent-webui/gateway/managed-gateway.mjs +41 -0
- package/packages/piagent-webui/gateway/managed-launch.ts +145 -0
- package/packages/piagent-webui/gateway/managed-projection.ts +20 -0
- package/packages/piagent-webui/gateway/pi-host.ts +22 -4
- package/packages/piagent-webui/gateway/runtime-session-controls.ts +5 -2
- package/packages/piagent-webui/gateway/session-approval-authority.ts +28 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +11 -3
- package/packages/piagent-webui/gateway/session-command-controller.ts +8 -2
- package/packages/piagent-webui/gateway/session-file-facts.ts +25 -0
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +42 -12
- package/packages/piagent-webui/gateway/session-list-cache.ts +68 -0
- package/packages/piagent-webui/gateway/session-runtime-ownership.ts +8 -1
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +20 -27
- package/packages/piagent-webui/ownership/managed-terminal-lease.ts +27 -0
- package/packages/piagent-webui/ownership/profile-state.ts +2 -0
- package/packages/piagent-webui/ownership/session-lease-store.ts +22 -1
- package/packages/piagent-webui/server/approval-projection.ts +9 -0
- package/packages/piagent-webui/server/core-inspection-provider.ts +4 -9
- package/packages/piagent-webui/server/loopback-server.ts +37 -1
- package/packages/piagent-webui/server/message-stop-reason.ts +11 -0
- package/packages/piagent-webui/server/tool-call-summary.ts +185 -0
- package/packages/piagent-webui/server/transcript-projection.ts +86 -3
- package/packs/engineering-base/pack.json +4 -0
- package/schemas/piagent-webui/runtime-command-v1.schema.json +1 -2
- package/schemas/piagent-webui/session-catalog-v1.schema.json +8 -0
- package/schemas/piagent-webui/transcript-v1.schema.json +94 -1
- package/scripts/build-docs-site.mjs +23 -12
- package/scripts/check-public-wording.mjs +5 -6
- package/scripts/check-runtime-advisories.mjs +23 -6
- package/scripts/claude-model-additions.mjs +33 -0
- package/scripts/configure-model-scope.sh +36 -8
- package/scripts/configure-subagents.sh +1 -1
- package/scripts/explain-command.mjs +5 -6
- package/scripts/install-global.sh +2 -2
- package/scripts/model-route-evaluation-core.ts +1 -0
- package/scripts/model-scope-prune.mjs +75 -0
- package/scripts/phase-tools-evaluation.mjs +9 -4
- package/scripts/pi-model-route.mjs +1 -1
- package/scripts/piagent-cli.mjs +51 -2
- package/scripts/piagent-dashboard.mjs +27 -4
- package/scripts/piagent-installations.mjs +26 -0
- package/scripts/piagent-studio.mjs +155 -0
- package/scripts/runtime-advisory-exceptions.json +29 -0
- package/scripts/setup.sh +2 -2
- package/scripts/team-doctor.sh +20 -1
- package/scripts/typescript-loader.mjs +6 -0
- package/scripts/verify-local.sh +5 -5
- package/scripts/verify-release-identity.mjs +1 -1
- package/templates/global/settings.json +8 -2
- package/templates/project/.pi/project-context.md +3 -3
- package/templates/project/AGENTS.md +4 -5
- package/docs/piagent-core-reasoning-research-2026-09-06.md +0 -225
- package/docs/piagent-core-research-sources-2026-09-06.json +0 -416
- package/packages/piagent-webui/dist/client/assets/index-DiKRmCEP.js +0 -176
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { managedProjection } from './managed-projection.ts';
|
|
1
2
|
import { randomBytes } from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -16,11 +17,14 @@ import {
|
|
|
16
17
|
writeGatewayDescriptor,
|
|
17
18
|
type GatewayDescriptor
|
|
18
19
|
} from "./profile-state.ts";
|
|
19
|
-
import { buildSessionCatalog } from "./session-catalog.ts";
|
|
20
|
+
import { buildSessionCatalog, projectRefForCwd } from "./session-catalog.ts";
|
|
21
|
+
import { sessionRefForPath } from "../ownership/session-refs.ts";
|
|
20
22
|
import { SessionMetadataStore } from "./session-metadata-store.ts";
|
|
21
23
|
import { GatewayProtocolService } from "./gateway-protocol-service.ts";
|
|
22
24
|
import { SessionLeaseStore } from "./session-lease-store.ts";
|
|
23
25
|
import { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
|
|
26
|
+
import { cachedSessionLister } from "./session-list-cache.ts";
|
|
27
|
+
import { cachedSessionFacts } from "./session-file-facts.ts";
|
|
24
28
|
import { buildSessionLiveState } from "./session-live-state.ts";
|
|
25
29
|
import { SessionCommandStore } from "./session-command-store.ts";
|
|
26
30
|
import { SessionCommandController } from "./session-command-controller.ts";
|
|
@@ -30,9 +34,10 @@ import { SessionInspectionRegistry } from "./session-inspection-registry.ts";
|
|
|
30
34
|
import { SessionAttachmentRegistry } from "./session-attachment-registry.ts";
|
|
31
35
|
import { ProjectRegistry } from "./project-registry.ts";
|
|
32
36
|
import { pickNativeProjectFolders } from "./native-project-picker.ts";
|
|
37
|
+
import { CompanyRelay, type CompanyConnector } from "./company-relay.ts";
|
|
33
38
|
import { ProviderAuthBroker } from "./provider-auth-broker.ts";
|
|
34
39
|
import { McpAuthBroker } from "./mcp-auth-broker.ts";
|
|
35
|
-
import type { ScopedBrokerRouter } from "./session-runtime-factory.ts";
|
|
40
|
+
import type { ScopedBrokerRouter, RuntimeFactory } from "./session-runtime-factory.ts";
|
|
36
41
|
|
|
37
42
|
function unavailable(reasonCode: string) {
|
|
38
43
|
return { status: "unavailable" as const, version: null, reasonCode };
|
|
@@ -44,7 +49,7 @@ function safeModelLabel(value: unknown): string {
|
|
|
44
49
|
return (redactSensitiveText(String(value ?? "Model")).text.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim() || "Model").slice(0, 120);
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
function capabilities(gatewayInstanceRef: string, runtimeAvailable = false): PiagentGatewayCapabilityHandshakeV1 {
|
|
52
|
+
function capabilities(gatewayInstanceRef: string, runtimeAvailable = false, managed = false): PiagentGatewayCapabilityHandshakeV1 {
|
|
48
53
|
return {
|
|
49
54
|
schemaVersion: 1,
|
|
50
55
|
version: "piagent-gateway-capabilities-v1",
|
|
@@ -61,9 +66,9 @@ function capabilities(gatewayInstanceRef: string, runtimeAvailable = false): Pia
|
|
|
61
66
|
create: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
|
|
62
67
|
send: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
|
|
63
68
|
abort: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
|
|
64
|
-
setModel: runtimeAvailable ? available() : unavailable("
|
|
69
|
+
setModel: runtimeAvailable && !managed ? available() : unavailable("model-managed-by-studio"),
|
|
65
70
|
setThinking: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
|
|
66
|
-
setPermission: runtimeAvailable ? available() : unavailable("
|
|
71
|
+
setPermission: runtimeAvailable && !managed ? available() : unavailable("permissions-managed-by-studio"),
|
|
67
72
|
rename: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
|
|
68
73
|
pin: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
|
|
69
74
|
archive: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
|
|
@@ -77,12 +82,20 @@ function capabilities(gatewayInstanceRef: string, runtimeAvailable = false): Pia
|
|
|
77
82
|
};
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
function packageVersion(root: string): string | undefined {
|
|
86
|
+
try { return String(JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version); } catch { return undefined; }
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
export async function startPiagentGateway(options: {
|
|
81
90
|
packageRoot: string;
|
|
82
91
|
expectedPiVersion: string;
|
|
83
92
|
agentDir?: string;
|
|
84
93
|
staticRoot?: string;
|
|
85
94
|
scopedBrokerRouter?: ScopedBrokerRouter;
|
|
95
|
+
managed?: { host: any; models: any; runtimeFactory: RuntimeFactory; project: string | null };
|
|
96
|
+
// Company sessions relayed into this dashboard (the dashboard entry passes
|
|
97
|
+
// the Agent Watch binding; omitted: none).
|
|
98
|
+
company?: CompanyConnector;
|
|
86
99
|
}): Promise<{ descriptor: GatewayDescriptor; wait(): Promise<void>; close(): Promise<void> }> {
|
|
87
100
|
const state = gatewayProfileState(options.agentDir);
|
|
88
101
|
process.env.PI_CODING_AGENT_DIR = state.agentDir;
|
|
@@ -99,6 +112,7 @@ export async function startPiagentGateway(options: {
|
|
|
99
112
|
let runtimes: SessionRuntimeSupervisor | null = null;
|
|
100
113
|
let mcpAuth: McpAuthBroker | null = null;
|
|
101
114
|
let attachments: SessionAttachmentRegistry | null = null;
|
|
115
|
+
let relay: CompanyRelay | null = null;
|
|
102
116
|
let closing: Promise<void> | null = null;
|
|
103
117
|
let settleWait: (() => void) | null = null;
|
|
104
118
|
const waited = new Promise<void>((resolve) => { settleWait = resolve; });
|
|
@@ -107,6 +121,7 @@ export async function startPiagentGateway(options: {
|
|
|
107
121
|
if (closing) return await closing;
|
|
108
122
|
closing = (async () => {
|
|
109
123
|
removeGatewayDescriptor(state, gatewayInstanceRef);
|
|
124
|
+
relay?.close();
|
|
110
125
|
await loopback?.close().catch(() => undefined);
|
|
111
126
|
// Staged bytes are private temp files. Closing deletes them rather than
|
|
112
127
|
// leaving a directory per session behind for the TTL sweep that will never
|
|
@@ -129,14 +144,26 @@ export async function startPiagentGateway(options: {
|
|
|
129
144
|
if (!loopback) return { ok: false, error: "gateway-starting" };
|
|
130
145
|
return reply({ launchUrl: loopback.issueLaunchUrl(), gatewayInstanceRef });
|
|
131
146
|
}
|
|
147
|
+
if (request.action === "project.register" || request.action === "project.paths") {
|
|
148
|
+
if (!options.managed || !runtimes) return { ok: false, error: "project-control-unavailable" };
|
|
149
|
+
if (request.action === "project.register") {
|
|
150
|
+
try { return reply(projects.register(request.cwd)); } catch { return { ok: false, error: "project-import-folder-invalid" }; }
|
|
151
|
+
}
|
|
152
|
+
// Registered folders and the folders of existing sessions.
|
|
153
|
+
return runtimes.listSessions().then((sessions) => {
|
|
154
|
+
const paths = new Map(projects.list().map((item) => [item.projectRef, projects.resolve(item.projectRef)]));
|
|
155
|
+
for (const info of sessions) if (typeof info.cwd === "string") paths.set(projectRefForCwd(key, info.cwd), info.cwd);
|
|
156
|
+
return reply([...paths].filter(([, cwd]) => cwd).map(([projectRef, cwd]) => ({ projectRef, cwd })));
|
|
157
|
+
}, () => ({ ok: false as const, error: "project-control-unavailable" }));
|
|
158
|
+
}
|
|
132
159
|
setImmediate(() => { void close(); });
|
|
133
160
|
return reply({ stopping: true, gatewayInstanceRef });
|
|
134
161
|
}
|
|
135
162
|
});
|
|
136
163
|
|
|
137
164
|
try {
|
|
138
|
-
const host = await loadPinnedPiHost(options.expectedPiVersion);
|
|
139
|
-
const inspectionModels = await host.ModelRuntime.create({
|
|
165
|
+
const host = options.managed?.host ?? await loadPinnedPiHost(options.expectedPiVersion);
|
|
166
|
+
const inspectionModels = options.managed?.models ?? await host.ModelRuntime.create({
|
|
140
167
|
authPath: path.join(state.agentDir, "auth.json"),
|
|
141
168
|
modelsPath: path.join(state.agentDir, "models.json"),
|
|
142
169
|
allowModelNetwork: false
|
|
@@ -145,12 +172,21 @@ export async function startPiagentGateway(options: {
|
|
|
145
172
|
mcpAuth = new McpAuthBroker(state.agentDir);
|
|
146
173
|
const leases = new SessionLeaseStore(state.root, key);
|
|
147
174
|
const events = new GatewayEventStore();
|
|
175
|
+
if (options.managed?.project) projects.register(options.managed.project);
|
|
148
176
|
runtimes = new SessionRuntimeSupervisor({
|
|
149
|
-
gatewayInstanceRef, key, leases, listSessions: () => host.SessionManager.listAll(),
|
|
177
|
+
gatewayInstanceRef, key, leases, listSessions: cachedSessionLister((dir) => host.SessionManager.listAll(dir), path.join(state.agentDir, 'sessions'), Boolean(options.managed)),
|
|
150
178
|
host, agentDir: state.agentDir, packageRoot: options.packageRoot, modelRuntime: inspectionModels, events,
|
|
179
|
+
runtimeFactory: options.managed?.runtimeFactory,
|
|
180
|
+
sessionDirectory: options.managed ? path.join(state.agentDir, 'sessions') : undefined,
|
|
151
181
|
scopedBrokerRouter: options.scopedBrokerRouter,
|
|
152
182
|
resolveProject: (projectRef) => projects.resolve(projectRef)
|
|
153
183
|
});
|
|
184
|
+
const sessionFacts = (manager: { buildSessionContext(): any; getBranch(): any[] }) => {
|
|
185
|
+
const context = manager.buildSessionContext();
|
|
186
|
+
return { model: context.model ? { provider: String(context.model.provider), modelId: String(context.model.modelId) } : null,
|
|
187
|
+
thinkingLevel: context.thinkingLevel, managed: managedProjection(context, manager.getBranch()) };
|
|
188
|
+
};
|
|
189
|
+
const fileFacts = cachedSessionFacts((file) => sessionFacts(host.SessionManager.open(file)));
|
|
154
190
|
const readCatalog = () => buildSessionCatalog({
|
|
155
191
|
gatewayInstanceRef,
|
|
156
192
|
key,
|
|
@@ -159,12 +195,15 @@ export async function startPiagentGateway(options: {
|
|
|
159
195
|
readOwnership: (sessionRef) => runtimes!.ownership(sessionRef),
|
|
160
196
|
readSessionOptions: (info) => {
|
|
161
197
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
198
|
+
// A session running here may not have flushed its model and thinking
|
|
199
|
+
// entries yet; its live manager has them, the file does not.
|
|
200
|
+
const live = runtimes!.liveSessionManager(sessionRefForPath(key, info.path));
|
|
201
|
+
const facts = live ? sessionFacts(live) : fileFacts(info.path);
|
|
202
|
+
const model = facts.model ? inspectionModels.getModel(facts.model.provider, facts.model.modelId) : null;
|
|
203
|
+
const thinking = ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(facts.thinkingLevel))
|
|
204
|
+
? facts.thinkingLevel : "unknown";
|
|
205
|
+
return { modelLabel: model ? safeModelLabel(model.name ?? model.id ?? facts.model?.modelId) : null,
|
|
206
|
+
thinkingLevel: thinking, ...facts.managed };
|
|
168
207
|
} catch { return { modelLabel: null, thinkingLevel: "unknown" }; }
|
|
169
208
|
}
|
|
170
209
|
});
|
|
@@ -173,7 +212,16 @@ export async function startPiagentGateway(options: {
|
|
|
173
212
|
if (catalog.state !== "ready" || !session) throw new Error("session-projection-unavailable");
|
|
174
213
|
return { sessionRevision: session.sessionRevision, liveState: session.liveState };
|
|
175
214
|
});
|
|
176
|
-
|
|
215
|
+
relay = options.managed || !options.company ? null : new CompanyRelay({
|
|
216
|
+
connector: options.company,
|
|
217
|
+
events, hubProject: (cwd) => projectRefForCwd(key, cwd),
|
|
218
|
+
folder: async (projectRef) => projects.resolve(projectRef)
|
|
219
|
+
?? (await runtimes!.listSessions()).find((info) => projectRefForCwd(key, info.cwd) === projectRef)?.cwd ?? null });
|
|
220
|
+
// Every revision the browser sees and sends back is the merged one.
|
|
221
|
+
const hubCatalog = relay ? async () => relay!.merge(await readCatalog()) : readCatalog;
|
|
222
|
+
const company = (sessionRef: unknown) => !!relay?.owns(sessionRef);
|
|
223
|
+
const relayPost = (path: string, body: unknown) => relay!.json("POST", path, body);
|
|
224
|
+
const commands = new SessionCommandController({ catalog: hubCatalog, runtimes, metadata,
|
|
177
225
|
store: new SessionCommandStore(state.root, key), events,
|
|
178
226
|
// Resolved at call time: the attachment registry is built after this
|
|
179
227
|
// controller, because it reads sessions through the inspection registry.
|
|
@@ -182,8 +230,9 @@ export async function startPiagentGateway(options: {
|
|
|
182
230
|
return attachments.reserveForPrompt(sessionRef, refs, messageRequestId, text);
|
|
183
231
|
} });
|
|
184
232
|
const runtimeCommands = new RuntimeCommandController({ catalog: readCatalog, runtimes, events });
|
|
185
|
-
const protocol = new GatewayProtocolService({ capabilities: () => capabilities(gatewayInstanceRef, true), catalog:
|
|
186
|
-
events, command:
|
|
233
|
+
const protocol = new GatewayProtocolService({ capabilities: () => capabilities(gatewayInstanceRef, true, !!options.managed), catalog: hubCatalog,
|
|
234
|
+
events, command: relay ? { execute: (command) => relay!.handles(command)
|
|
235
|
+
? relay!.command(command as Record<string, unknown>, async () => (await hubCatalog()).catalogRevision) : commands.execute(command) } : commands });
|
|
187
236
|
const inspections = new SessionInspectionRegistry({ gatewayInstanceRef, host, key, packageRoot: options.packageRoot, agentDir: state.agentDir,
|
|
188
237
|
models: inspectionModels, projects, mcpAuth, listSessions: () => runtimes!.listSessions(),
|
|
189
238
|
openLiveSession: (sessionRef) => runtimes!.liveSessionManager(sessionRef),
|
|
@@ -196,22 +245,51 @@ export async function startPiagentGateway(options: {
|
|
|
196
245
|
loopback = await startLoopbackServer({
|
|
197
246
|
staticRoot,
|
|
198
247
|
mode: "gateway",
|
|
199
|
-
readCapabilities: () => capabilities(gatewayInstanceRef, true),
|
|
200
|
-
readSessionCatalog:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
248
|
+
readCapabilities: () => capabilities(gatewayInstanceRef, true, !!options.managed),
|
|
249
|
+
readSessionCatalog: hubCatalog,
|
|
250
|
+
// Company settlements arrive as re-published events; running company
|
|
251
|
+
// operations are read from its Gateway.
|
|
252
|
+
readSessionLiveState: async () => {
|
|
253
|
+
const companyOperations = relay ? await relay.operations() : [];
|
|
254
|
+
const live = buildSessionLiveState({ gatewayInstanceRef, eventSequence: events.stateVersion,
|
|
255
|
+
operations: runtimes!.currentOperations(), settlements: events.recentOperationSettlements() });
|
|
256
|
+
return companyOperations.length ? { ...live, operations: [...live.operations, ...companyOperations] } : live;
|
|
257
|
+
},
|
|
258
|
+
readSessionCreationOptions: async () => {
|
|
259
|
+
if (!relay) return inspections.creationOptions();
|
|
260
|
+
// A folder with company conversations is a dashboard project too, so
|
|
261
|
+
// a new chat there (company or personal) can be started from here.
|
|
262
|
+
for (const folder of await relay.folders()) { try { projects.register(folder); } catch { /* moved or unreadable */ } }
|
|
263
|
+
const options = await inspections.creationOptions() as { models: unknown[] };
|
|
264
|
+
return { ...options, models: [...await relay.models(), ...options.models] };
|
|
265
|
+
},
|
|
266
|
+
relaySessionRead: relay ? async (sessionRef, path) => company(sessionRef)
|
|
267
|
+
? await relay!.http("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/inspection${path}`) : null : undefined,
|
|
204
268
|
readSessionModel: (sessionRef) => inspections.provider(sessionRef),
|
|
205
269
|
readSessionConnections: (sessionRef) => inspections.connections(sessionRef),
|
|
206
|
-
executeSessionConnection: (command) =>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
270
|
+
executeSessionConnection: (command) => {
|
|
271
|
+
if (options.managed) throw new Error('connections-managed-by-studio');
|
|
272
|
+
return company((command as { sessionRef?: unknown } | null)?.sessionRef) ? relayPost("/api/v1/session-connections", command)
|
|
273
|
+
: inspections.executeConnectionCommand(command);
|
|
274
|
+
},
|
|
275
|
+
executeRuntimeCommand: (command) => company((command as { sessionRef?: unknown } | null)?.sessionRef)
|
|
276
|
+
? relayPost("/api/v1/runtime-commands", command) : runtimeCommands.execute(command),
|
|
277
|
+
executeSessionAttachment: (sessionRef, command) => company(sessionRef)
|
|
278
|
+
? relayPost(`/api/v1/sessions/${encodeURIComponent(sessionRef)}/attachments`, command) : attachments.execute(sessionRef, command),
|
|
279
|
+
// Approval refs carry no session; a company approval is not pending here.
|
|
280
|
+
executeApproval: async (approvalRef, decision) => {
|
|
281
|
+
try { return await runtimes!.decideApproval(approvalRef, decision); }
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (!relay || (error as Error).message !== "approval-not-pending") throw error;
|
|
284
|
+
return await relayPost(`/api/v1/approvals/${encodeURIComponent(approvalRef)}/decision`, decision);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
210
287
|
readMcpAuthJob: (jobRef) => mcpAuth!.read(jobRef),
|
|
211
288
|
cancelMcpAuthJob: (jobRef) => mcpAuth!.cancel(jobRef),
|
|
212
|
-
readProviderAuthCatalog: () => providerAuth.catalog(),
|
|
289
|
+
readProviderAuthCatalog: () => options.managed ? { ...providerAuth.catalog(), providers: [], reasonCode: 'personal-oauth-requires-personal-session' } : providerAuth.catalog(),
|
|
213
290
|
readProviderAuthJob: (jobRef) => providerAuth.read(jobRef),
|
|
214
291
|
executeProviderAuth: (command) => {
|
|
292
|
+
if (options.managed) throw new Error('personal-oauth-requires-personal-session');
|
|
215
293
|
const action = command && typeof command === "object" && !Array.isArray(command) ? (command as Record<string, unknown>).action : null;
|
|
216
294
|
if (action === "provider-auth.start") return providerAuth.start(command);
|
|
217
295
|
if (action === "provider-auth.respond") return providerAuth.respond(command);
|
|
@@ -225,6 +303,7 @@ export async function startPiagentGateway(options: {
|
|
|
225
303
|
events.publish("catalog.changed", { reasonCode: "project-imported" });
|
|
226
304
|
return { schemaVersion: 1, version: "piagent-project-import-result-v1", importedAt: new Date().toISOString(), project, projects: registered };
|
|
227
305
|
},
|
|
306
|
+
...(relay ? { readCompanyStatus: () => relay!.status(), executeCompanyConnect: async () => { await relay!.connect(true); return relay!.status(); } } : {}),
|
|
228
307
|
gatewayProtocol: protocol
|
|
229
308
|
});
|
|
230
309
|
descriptor = {
|
|
@@ -234,9 +313,11 @@ export async function startPiagentGateway(options: {
|
|
|
234
313
|
startedAt: new Date().toISOString(),
|
|
235
314
|
origin: loopback.origin,
|
|
236
315
|
controlSocket: state.controlSocket,
|
|
237
|
-
profileRef: profileRef(state, key)
|
|
316
|
+
profileRef: profileRef(state, key),
|
|
317
|
+
packageVersion: packageVersion(options.packageRoot)
|
|
238
318
|
};
|
|
239
319
|
writeGatewayDescriptor(state, descriptor);
|
|
320
|
+
relay?.start();
|
|
240
321
|
} catch (error) {
|
|
241
322
|
await close();
|
|
242
323
|
throw error;
|
|
@@ -3,6 +3,7 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
|
|
4
4
|
import { hasVisibleText } from "../shared/text-visibility.ts";
|
|
5
5
|
import type { TerminalDeliveryReceipt } from "../server/terminal-delivery-receipt.ts";
|
|
6
|
+
import { effectiveStopReason } from "../server/message-stop-reason.ts";
|
|
6
7
|
import { GatewayEventStore, type GatewayTaskStatus } from "./gateway-events.ts";
|
|
7
8
|
import { SessionOperationLifecycle, type SessionOperationObservation,
|
|
8
9
|
type SessionOperationPhase, type SessionOperationRetryPolicyOptions } from "./session-operation-lifecycle.ts";
|
|
@@ -103,7 +104,7 @@ function messageText(message: any): string {
|
|
|
103
104
|
.map((item: any) => item.text).join("\n");
|
|
104
105
|
}
|
|
105
106
|
function assistantSettlement(message: any): Settlement {
|
|
106
|
-
const stopReason =
|
|
107
|
+
const stopReason = effectiveStopReason(message);
|
|
107
108
|
if (stopReason === "aborted") return { outcome: "aborted", reasonCode: "operation-aborted" };
|
|
108
109
|
if (stopReason === "error") return { outcome: "error", reasonCode: "assistant-response-failed" };
|
|
109
110
|
if (stopReason === "length") return { outcome: "error", reasonCode: "assistant-output-incomplete" };
|
|
@@ -159,6 +160,8 @@ export class GatewaySessionStream {
|
|
|
159
160
|
readonly #commitTerminalDeliveryReceipt: ((message: unknown) => TerminalDeliveryReceipt | undefined) | undefined;
|
|
160
161
|
#terminalDeliveryMessage: unknown;
|
|
161
162
|
#receiptOnly = true;
|
|
163
|
+
#compactionRef: string | null = null;
|
|
164
|
+
readonly #capacityRefs = new Map<string, string>();
|
|
162
165
|
|
|
163
166
|
get runtimeRestartRequired(): boolean { return this.#runtimeRestartRequired; }
|
|
164
167
|
get lifecycleTerminationReasonCode(): string | null { return this.#lifecycleTerminationReasonCode; }
|
|
@@ -204,7 +207,17 @@ export class GatewaySessionStream {
|
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
observe(event: any): SessionOperationObservation {
|
|
210
|
+
// A company subagent started or ended while the main agent was quiet: its
|
|
211
|
+
// count in the catalog row ("Subagent 1/2") changed, and no agent event says so.
|
|
212
|
+
if (event?.type === "managed_helpers") this.#events.publish("catalog.changed", { reasonCode: "managed-helpers-changed" });
|
|
207
213
|
const wasStarted = this.#lifecycle.started, wasSettled = this.#lifecycle.hostSettled;
|
|
214
|
+
// Summarising the older part of a long conversation can take a minute,
|
|
215
|
+
// right before the model is asked or after it answered: shown as a step
|
|
216
|
+
// of the running turn, not as silence.
|
|
217
|
+
if (event?.type === "compaction_start" || event?.type === "compaction_end") this.#compaction(event);
|
|
218
|
+
// A company request waiting in line for a free model account: a step of
|
|
219
|
+
// the running turn until Studio admits it, it gives up or Stop ends it.
|
|
220
|
+
if (event?.type === "managed_capacity_wait") this.#capacityWait(event);
|
|
208
221
|
const observation = this.#lifecycle.observe(event);
|
|
209
222
|
if (!observation.accepted) return observation;
|
|
210
223
|
if (event?.type === "agent_start" || event?.message?.role === "assistant" || event?.assistantMessageEvent
|
|
@@ -255,6 +268,38 @@ export class GatewaySessionStream {
|
|
|
255
268
|
return observation;
|
|
256
269
|
}
|
|
257
270
|
|
|
271
|
+
#compaction(event: any): void {
|
|
272
|
+
if (event.type === "compaction_start") {
|
|
273
|
+
if (this.#compactionRef) return;
|
|
274
|
+
this.#compactionRef = ref("tool");
|
|
275
|
+
this.#events.publish("tool.started", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(),
|
|
276
|
+
toolCallRef: this.#compactionRef, toolLabel: "compaction", fileLabel: null, isError: null, reasonCode: null });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (!this.#compactionRef) return;
|
|
280
|
+
const failed = typeof event.errorMessage === "string" && event.errorMessage.length > 0;
|
|
281
|
+
this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(),
|
|
282
|
+
toolCallRef: this.#compactionRef, toolLabel: "compaction", fileLabel: null, isError: failed, reasonCode: failed ? "compaction-failed" : null });
|
|
283
|
+
this.#compactionRef = null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#capacityWait(event: any): void {
|
|
287
|
+
const id = typeof event.id === "string" ? event.id : null;
|
|
288
|
+
if (!id) return;
|
|
289
|
+
if (event.state === "start" && !this.#capacityRefs.has(id)) {
|
|
290
|
+
const toolCallRef = ref("tool"); this.#capacityRefs.set(id, toolCallRef);
|
|
291
|
+
this.#events.publish("tool.started", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(),
|
|
292
|
+
toolCallRef, toolLabel: "capacity-wait", fileLabel: null, isError: null, reasonCode: null });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const toolCallRef = this.#capacityRefs.get(id);
|
|
296
|
+
if (event.state !== "end" || !toolCallRef) return;
|
|
297
|
+
this.#capacityRefs.delete(id);
|
|
298
|
+
const failed = event.outcome === "gave-up";
|
|
299
|
+
this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(),
|
|
300
|
+
toolCallRef, toolLabel: "capacity-wait", fileLabel: null, isError: failed, reasonCode: failed ? "capacity-wait-expired" : null });
|
|
301
|
+
}
|
|
302
|
+
|
|
258
303
|
#settleActiveTools(reasonCode: string): void {
|
|
259
304
|
for (const [toolCallId, toolCallRef] of this.#toolRefs) {
|
|
260
305
|
this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(), toolCallRef,
|
|
@@ -262,6 +307,11 @@ export class GatewaySessionStream {
|
|
|
262
307
|
isError: true, reasonCode });
|
|
263
308
|
}
|
|
264
309
|
this.#toolRefs.clear(); this.#toolLabels.clear(); this.#toolFileLabels.clear();
|
|
310
|
+
for (const toolCallRef of this.#capacityRefs.values()) {
|
|
311
|
+
this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(), toolCallRef,
|
|
312
|
+
toolLabel: "capacity-wait", fileLabel: null, isError: false, reasonCode: null });
|
|
313
|
+
}
|
|
314
|
+
this.#capacityRefs.clear();
|
|
265
315
|
}
|
|
266
316
|
|
|
267
317
|
#flush(final: boolean): void {
|
|
@@ -270,7 +320,13 @@ export class GatewaySessionStream {
|
|
|
270
320
|
return;
|
|
271
321
|
}
|
|
272
322
|
let take = final ? this.#buffer.length : this.#buffer.lastIndexOf("\n") + 1;
|
|
273
|
-
|
|
323
|
+
// A long line goes out in pieces cut after a space. Credentials hold no
|
|
324
|
+
// space, so one still arriving is never sent half-formed (redaction knows
|
|
325
|
+
// a token only by its whole shape); a blob without any space is sent at 16 KiB.
|
|
326
|
+
if (take <= 0 && this.#buffer.length >= 1_024) {
|
|
327
|
+
const space = this.#buffer.search(/\s\S*$/);
|
|
328
|
+
take = space >= 0 ? space + 1 : this.#buffer.length >= 16_384 ? this.#buffer.length : 0;
|
|
329
|
+
}
|
|
274
330
|
if (take <= 0) return;
|
|
275
331
|
const candidate = this.#buffer.slice(0, take), privateStart = candidate.search(PRIVATE_BEGIN);
|
|
276
332
|
if (privateStart >= 0 && !PRIVATE_END.test(candidate.slice(privateStart))) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { managedThinkingLevels } from '../../piagent-core/managed/capabilities.mjs';
|
|
2
|
+
import { ManagedBrokerClient } from '../../piagent-core/managed/broker-client.mjs';
|
|
3
|
+
import { ManagedSession } from '../../piagent-core/managed/session.mjs';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
|
|
6
|
+
// Only called by the pinned launcher with Watch's non-secret import manifest.
|
|
7
|
+
// Each web session owns its own broker; no personal auth/model/resource loader.
|
|
8
|
+
export async function startManagedGateway({ config, configPath, cwd, agentDir, packageRoot, registerProject = true }) {
|
|
9
|
+
// Agent Watch may hold another key of this member since the Gateway
|
|
10
|
+
// started: a new conversation, or one that enrolls again, uses that key.
|
|
11
|
+
const slot=()=>{try{const now=JSON.parse(fs.readFileSync(configPath,'utf8'));return /^[a-f0-9]{64}$/.test(now.profile_id)&&now.broker===config.broker?now.profile_id:config.profile_id;}catch{return config.profile_id;}};
|
|
12
|
+
const newBroker=()=>new ManagedBrokerClient({executable:config.broker,profileID:slot()});
|
|
13
|
+
// Agent Watch rewrites the binding's revision when the key or its harness
|
|
14
|
+
// changes; a warm conversation then enrolls again before its next run.
|
|
15
|
+
const bindingRevision=()=>{try{return String(JSON.parse(fs.readFileSync(configPath,'utf8')).configuration_revision??'')||null;}catch{return null;}};
|
|
16
|
+
const probeBroker = newBroker();
|
|
17
|
+
let probe;
|
|
18
|
+
try {
|
|
19
|
+
probe = await ManagedSession.create({sdkRoot:config.sdk_root,cwd,origin:config.origin,broker:probeBroker,agentDir,protectedRoots:[configPath,agentDir]});
|
|
20
|
+
const { startPiagentGateway } = await import('./gateway-service.ts');
|
|
21
|
+
const { rpcUiContext } = await import('./rpc-ui-context.ts');
|
|
22
|
+
const { webUiModelRef } = await import('../../piagent-core/runtime/inspection/webui-snapshot.ts');
|
|
23
|
+
const host=probe.api,models=probe.modelRuntime;
|
|
24
|
+
const snapshot=models.getAvailableSnapshot.bind(models);
|
|
25
|
+
models.getAvailableSnapshot=()=>snapshot().map(model=>({...model,managedThinkingLevels:managedThinkingLevels(probe.manifest,models,model)}));
|
|
26
|
+
const factory=async(info,_instance,manager,initial)=>{
|
|
27
|
+
if(initial?.modelRef && initial.modelRef!==webUiModelRef('agent_watch_managed','agent-watch-auto')) throw Error('managed-personal-switch-requires-new-session');
|
|
28
|
+
const broker=newBroker();
|
|
29
|
+
let runtime;
|
|
30
|
+
try {
|
|
31
|
+
runtime=await ManagedSession.create({sdkRoot:config.sdk_root,cwd:info.cwd,origin:config.origin,broker,agentDir,
|
|
32
|
+
sessionManager:manager??host.SessionManager.open(info.path),protectedRoots:[configPath,agentDir],renewBroker:newBroker,bindingRevision});
|
|
33
|
+
if(initial) runtime.session.setThinkingLevel(initial.thinkingLevel);
|
|
34
|
+
await runtime.session.bindExtensions({mode:'rpc',uiContext:rpcUiContext()});
|
|
35
|
+
return {session:runtime.session,dispose:()=>runtime.dispose()};
|
|
36
|
+
} catch(error) { if(runtime) await runtime.dispose(); else await broker.dispose(); throw error; }
|
|
37
|
+
};
|
|
38
|
+
const gateway=await startPiagentGateway({packageRoot,agentDir,expectedPiVersion:'0.87.1',managed:{host,models,runtimeFactory:factory,project:registerProject?cwd:null}});
|
|
39
|
+
return gateway;
|
|
40
|
+
} finally { if(probe) await probe.dispose(); else await probeBroker.dispose(); }
|
|
41
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { requestGatewayControl } from "./control-socket.ts";
|
|
8
|
+
import { gatewayProfileState } from "./profile-state.ts";
|
|
9
|
+
import { storedSlot } from "../../piagent-core/managed/store.mjs";
|
|
10
|
+
|
|
11
|
+
// The personal dashboard shows company sessions but never runs them: it starts
|
|
12
|
+
// the pinned managed entrypoint (its own process and Gateway, `--serve`) the
|
|
13
|
+
// way Agent Watch's launcher does, then relays to it. Grants stay inside that
|
|
14
|
+
// process; this process only reads the public binding.
|
|
15
|
+
export type ManagedLaunchStatus = { schemaVersion: 1; version: "piagent-managed-launch-status-v1"; available: boolean; model: "agent-watch-auto" };
|
|
16
|
+
type Binding = { file: string; profile: string; node: string; entrypoint: string; broker: string; brokerSha256: string };
|
|
17
|
+
|
|
18
|
+
const CLEAN_ENV = { PATH: "/usr/bin:/bin", LANG: "en_US.UTF-8", TERM: "xterm-256color" };
|
|
19
|
+
const READY = /^Agent Watch Auto: ready$/m;
|
|
20
|
+
const sha256 = (file: string) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
21
|
+
const HEX64 = /^[a-f0-9]{64}$/;
|
|
22
|
+
|
|
23
|
+
const DEFAULT_AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
|
|
24
|
+
|
|
25
|
+
// The relay and the company Gateway speak one protocol, so they must be the
|
|
26
|
+
// same Piagent release: Agent Watch may still point at another install (nvm,
|
|
27
|
+
// Homebrew…) and a company Gateway started before an update keeps running.
|
|
28
|
+
const versionAt = (file: string): string | null => { try { return String(JSON.parse(fs.readFileSync(file, "utf8")).version); } catch { return null; } };
|
|
29
|
+
const OWN_VERSION = versionAt(fileURLToPath(new URL("../../../package.json", import.meta.url)));
|
|
30
|
+
const sameRelease = (pinned: Binding) => OWN_VERSION === null || versionAt(path.join(path.dirname(path.dirname(pinned.entrypoint)), "package.json")) === OWN_VERSION;
|
|
31
|
+
const current = (health: { packageVersion?: unknown }) => OWN_VERSION === null || health.packageVersion === OWN_VERSION;
|
|
32
|
+
|
|
33
|
+
// Agent Watch writes the binding into the personal Pi folder it imported into.
|
|
34
|
+
function binding(agentDir: string): Binding | null {
|
|
35
|
+
const file = path.join(agentDir, "agent-watch-managed.json");
|
|
36
|
+
try {
|
|
37
|
+
const stat = fs.lstatSync(file);
|
|
38
|
+
if (!stat.isFile() || stat.size > 16_384) return null;
|
|
39
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
40
|
+
const absolute = (entry: unknown) => typeof entry === "string" && path.isAbsolute(entry);
|
|
41
|
+
if (value?.schema_version !== 1 || value.model !== "agent-watch-auto" || !HEX64.test(String(value.profile_id))
|
|
42
|
+
|| !absolute(value.node) || !absolute(value.entrypoint) || !absolute(value.broker) || !HEX64.test(String(value.broker_sha256))) return null;
|
|
43
|
+
return { file, profile: value.profile_id, node: value.node, entrypoint: value.entrypoint, broker: value.broker, brokerSha256: value.broker_sha256 };
|
|
44
|
+
} catch { return null; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function managedLaunchStatus(agentDir = DEFAULT_AGENT_DIR): ManagedLaunchStatus {
|
|
48
|
+
return { schemaVersion: 1, version: "piagent-managed-launch-status-v1", available: binding(agentDir) !== null, model: "agent-watch-auto" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A key's slot points at its member's conversation store once it has been
|
|
52
|
+
// launched (store.mjs); until then the slot's own folder is asked.
|
|
53
|
+
function controlSocket(home: string, profile: string): string {
|
|
54
|
+
const root = path.join(home, "Library/Application Support/AgentWatch/ManagedSessions");
|
|
55
|
+
return gatewayProfileState(path.join(root, storedSlot(root, profile) ?? profile)).controlSocket;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// "busy": the Gateway took the connection but has not answered yet. It is
|
|
59
|
+
// alive; starting another beside it would leave two on the same store.
|
|
60
|
+
async function running(socket: string): Promise<{ packageVersion?: unknown } | "busy" | null> {
|
|
61
|
+
try {
|
|
62
|
+
const reply = await requestGatewayControl(socket, { action: "health" });
|
|
63
|
+
return reply.ok ? (reply.value && typeof reply.value === "object" ? reply.value : {}) : null;
|
|
64
|
+
} catch (error) { return error instanceof Error && error.message === "gateway-control-timeout" ? "busy" : null; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Same foreground step as Agent Watch: macOS may ask once to let this build of
|
|
68
|
+
// the broker read its Keychain item. Nothing secret comes back. Exit 67 means
|
|
69
|
+
// the imported key is no longer connected in Agent Watch.
|
|
70
|
+
async function authorize(pinned: Binding): Promise<void> {
|
|
71
|
+
if (fs.realpathSync(pinned.broker) !== pinned.broker || sha256(pinned.broker) !== pinned.brokerSha256) throw new Error("managed-launch-binding-changed");
|
|
72
|
+
const code = await new Promise<number | null>((resolve) => {
|
|
73
|
+
const child = spawn(pinned.broker, ["managed-authorize", "--profile", pinned.profile], { cwd: "/", stdio: "ignore", env: CLEAN_ENV, timeout: 180_000, killSignal: "SIGKILL" });
|
|
74
|
+
child.once("error", () => resolve(null));
|
|
75
|
+
child.once("exit", (exit) => resolve(exit));
|
|
76
|
+
});
|
|
77
|
+
if (code === 67) throw new Error("managed-profile-disconnected");
|
|
78
|
+
if (code !== 0) throw new Error("managed-keychain-approval-required");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The control socket of a running company Gateway, without launching one.
|
|
82
|
+
export async function attachCompanyGateway(agentDir = DEFAULT_AGENT_DIR, home = os.homedir()): Promise<string | null> {
|
|
83
|
+
const pinned = binding(agentDir);
|
|
84
|
+
if (!pinned || !sameRelease(pinned)) return null;
|
|
85
|
+
const socket = controlSocket(home, pinned.profile);
|
|
86
|
+
const health = await running(socket);
|
|
87
|
+
return health && health !== "busy" && current(health) ? socket : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let inFlight: Promise<string> | null = null;
|
|
91
|
+
|
|
92
|
+
// Returns the company Gateway's control socket, starting it when needed.
|
|
93
|
+
export function ensureCompanyGateway(agentDir = DEFAULT_AGENT_DIR, home = os.homedir(), timeoutMs = 45_000): Promise<string> {
|
|
94
|
+
inFlight ??= start(agentDir, home, timeoutMs).finally(() => { inFlight = null; });
|
|
95
|
+
return inFlight;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function start(agentDir: string, home: string, timeoutMs: number): Promise<string> {
|
|
99
|
+
const pinned = binding(agentDir);
|
|
100
|
+
if (!pinned) throw new Error("managed-config-missing");
|
|
101
|
+
if (!sameRelease(pinned)) throw new Error("managed-launch-version-mismatch");
|
|
102
|
+
const socket = controlSocket(home, pinned.profile);
|
|
103
|
+
let health = await running(socket);
|
|
104
|
+
for (const started = Date.now(); health === "busy"; health = await running(socket)) {
|
|
105
|
+
if (Date.now() - started > timeoutMs) throw new Error("managed-launch-busy");
|
|
106
|
+
}
|
|
107
|
+
if (health && current(health)) return socket;
|
|
108
|
+
if (health) {
|
|
109
|
+
// A company Gateway from before the update: stop it (its transcripts stay
|
|
110
|
+
// on disk) and start this release.
|
|
111
|
+
try { await requestGatewayControl(socket, { action: "stop" }); } catch { /* already stopping */ }
|
|
112
|
+
for (const started = Date.now(); await running(socket);) {
|
|
113
|
+
if (Date.now() - started > 15_000) throw new Error("managed-launch-stale-runtime");
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
await authorize(pinned);
|
|
118
|
+
// Startup failures are reported on stderr as `Agent Watch: managed-…`; the
|
|
119
|
+
// log is private and removed once the process is ready or has exited.
|
|
120
|
+
const directory = path.join(home, "Library/Application Support/AgentWatch/ManagedLaunch");
|
|
121
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); fs.chmodSync(directory, 0o700);
|
|
122
|
+
const log = path.join(directory, `launch-${process.pid}-${Date.now()}.log`);
|
|
123
|
+
const fd = fs.openSync(log, "wx", 0o600);
|
|
124
|
+
let exited = false;
|
|
125
|
+
try {
|
|
126
|
+
const child = spawn(pinned.node, [pinned.entrypoint, "--config", pinned.file, "--serve"],
|
|
127
|
+
{ cwd: "/", detached: true, stdio: ["ignore", fd, fd], env: CLEAN_ENV });
|
|
128
|
+
child.once("exit", () => { exited = true; }); child.once("error", () => { exited = true; }); child.unref();
|
|
129
|
+
} finally { fs.closeSync(fd); }
|
|
130
|
+
try {
|
|
131
|
+
for (const started = Date.now(); Date.now() - started < timeoutMs;) {
|
|
132
|
+
const text = fs.readFileSync(log, "utf8");
|
|
133
|
+
// The launch may just have pointed this slot at the member's store.
|
|
134
|
+
if (READY.test(text)) return controlSocket(home, pinned.profile);
|
|
135
|
+
if (exited) throw new Error(text.match(/Agent Watch: (managed-[a-z:_-]+)\./)?.[1] ?? "managed-launch-failed");
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
137
|
+
}
|
|
138
|
+
throw new Error("managed-launch-timeout");
|
|
139
|
+
} finally { fs.rmSync(log, { force: true }); }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The personal dashboard's connector for company sessions (CompanyRelay).
|
|
143
|
+
export function agentWatchCompanyConnector(agentDir = DEFAULT_AGENT_DIR, home = os.homedir()) {
|
|
144
|
+
return { configured: () => binding(agentDir) !== null, ensure: () => ensureCompanyGateway(agentDir, home), attach: () => attachCompanyGateway(agentDir, home) };
|
|
145
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Public managed state contains no provider/model route or credential.
|
|
2
|
+
const STATUS = ['pending', 'in_progress', 'completed'];
|
|
3
|
+
const OUTCOMES = ['no_change', 'interrupted', 'blocking_open', 'unverified', 'review_unavailable', 'unreviewed', 'clean'];
|
|
4
|
+
export function managedProjection(context: any, entries: any[]) {
|
|
5
|
+
if (context?.model?.provider !== 'agent_watch_managed') return {};
|
|
6
|
+
const last = (type: string) => entries.filter(e => e.type === 'custom' && e.customType === type).at(-1)?.data;
|
|
7
|
+
const counter = last('agent-watch-helpers');
|
|
8
|
+
const capabilities = last('agent-watch-model')?.managedThinkingLevels;
|
|
9
|
+
const levels = ['off','minimal','low','medium','high','xhigh','max'];
|
|
10
|
+
// The agent's checklist (update_plan) and how its last code-changing turn ended.
|
|
11
|
+
const plan = last('agent-watch-plan')?.plan;
|
|
12
|
+
const steps = Array.isArray(plan) && plan.length >= 1 && plan.length <= 30 && plan.every((p: any) => typeof p?.step === 'string' && p.step.trim() && STATUS.includes(p.status))
|
|
13
|
+
? plan.map((p: any) => ({ step: p.step.trim().slice(0, 200), status: p.status })) : null;
|
|
14
|
+
const final = entries.filter(e => e.type === 'custom_message' && e.customType === 'agent-watch-process' && e.details?.phase === 'final').at(-1)?.details;
|
|
15
|
+
const process = final && OUTCOMES.includes(final.outcome) ? { outcome: final.outcome, verified: final.verified === true, reviewed: final.reviewed === true,
|
|
16
|
+
blockingOpen: Number.isInteger(final.blockingOpen) && final.blockingOpen >= 0 ? Math.min(final.blockingOpen, 1000) : 0, ...(final.planSkipped === true ? { planSkipped: true as const } : {}) } : null;
|
|
17
|
+
return {modelLabel:'agent-watch-auto', managedHelpers:{active:Number.isInteger(counter?.active) && counter.active>=0 && counter.active<=2 ? counter.active : 0, maximum:2 as const},
|
|
18
|
+
...(Array.isArray(capabilities)?{managedThinkingLevels:levels.filter(level=>capabilities.includes(level))}:{}),
|
|
19
|
+
...(steps ? {managedPlan:{steps}} : {}), ...(process ? {managedProcess:process} : {})};
|
|
20
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import { pathToFileURL } from "node:url";
|
|
@@ -27,10 +28,27 @@ export function installedPiHostRoot(): string {
|
|
|
27
28
|
} catch {
|
|
28
29
|
// The operator installation is normally global.
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
try {
|
|
32
|
+
const executable = execFileSync("which", ["pi"], { encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
33
|
+
const found = packageRootFrom(fs.realpathSync(executable));
|
|
34
|
+
if (found) return found;
|
|
35
|
+
} catch {
|
|
36
|
+
// A launcher with a short PATH (Agent Watch, launchd) cannot see `pi`.
|
|
37
|
+
}
|
|
38
|
+
// Then the host the installed CLI recorded, and the usual global prefixes.
|
|
39
|
+
const home = os.homedir(), hostPackage = path.join("lib", "node_modules", "@earendil-works", "pi-coding-agent");
|
|
40
|
+
let recorded: string | null = null;
|
|
41
|
+
try {
|
|
42
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR || path.join(home, ".pi", "agent");
|
|
43
|
+
recorded = JSON.parse(fs.readFileSync(path.join(agentDir, "piagent-runtime.json"), "utf8")).pi_sdk_root ?? null;
|
|
44
|
+
} catch { /* no record yet */ }
|
|
45
|
+
for (const candidate of [recorded, path.join(home, ".pi", "npm-global", hostPackage), path.join(home, ".local", hostPackage),
|
|
46
|
+
path.join("/opt/homebrew", hostPackage), path.join("/usr/local", hostPackage)]) {
|
|
47
|
+
if (!candidate) continue;
|
|
48
|
+
const found = packageRootFrom(path.join(candidate, "package.json"));
|
|
49
|
+
if (found) return found;
|
|
50
|
+
}
|
|
51
|
+
throw new Error("pi-host-unavailable");
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
export async function loadPinnedPiHost(expectedVersion: string): Promise<any> {
|