@piagent/platform 1.5.2 → 1.5.4
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 +54 -1
- package/README.md +8 -2
- package/README.vi.md +23 -1
- package/benchmarks/capability-v1/DESIGN.md +30 -0
- package/benchmarks/capability-v1/coverage.json +23 -1
- package/benchmarks/capability-v1/grade.mjs +246 -0
- package/benchmarks/capability-v1/project/apps/web/src/control-view.js +3 -0
- package/benchmarks/capability-v1/project/apps/web/src/timeline-view.js +3 -0
- package/benchmarks/capability-v1/project/packages/session/src/admission.js +7 -0
- package/benchmarks/capability-v1/project/packages/session/src/runtime-route.js +3 -0
- package/benchmarks/capability-v1/project/packages/timeline/src/checkpoint.js +7 -0
- package/benchmarks/capability-v1/project/packages/timeline/src/project.js +7 -0
- package/benchmarks/capability-v1/project/test/smoke.test.js +19 -1
- package/benchmarks/capability-v1/prompts/causal-timeline-recovery.md +47 -0
- package/benchmarks/capability-v1/prompts/durable-session-control-plane.md +47 -0
- package/benchmarks/capability-v1/references/causal-timeline-recovery/apps/web/src/timeline-view.js +9 -0
- package/benchmarks/capability-v1/references/causal-timeline-recovery/packages/timeline/src/checkpoint.js +22 -0
- package/benchmarks/capability-v1/references/causal-timeline-recovery/packages/timeline/src/project.js +77 -0
- package/benchmarks/capability-v1/references/durable-session-control-plane/apps/web/src/control-view.js +4 -0
- package/benchmarks/capability-v1/references/durable-session-control-plane/packages/session/src/admission.js +52 -0
- package/benchmarks/capability-v1/references/durable-session-control-plane/packages/session/src/runtime-route.js +27 -0
- package/benchmarks/capability-v1/rubric.json +26 -0
- package/benchmarks/capability-v1/suite.json +32 -4
- package/benchmarks/capability-v1/variant.mjs +11 -0
- package/benchmarks/deep-logic-v1/grade.mjs +165 -0
- package/benchmarks/deep-logic-v1/project/README.md +3 -0
- package/benchmarks/deep-logic-v1/project/package.json +6 -0
- package/benchmarks/deep-logic-v1/project/src/config-transaction.js +3 -0
- package/benchmarks/deep-logic-v1/project/src/context-pack.js +3 -0
- package/benchmarks/deep-logic-v1/project/src/event-reconcile.js +3 -0
- package/benchmarks/deep-logic-v1/project/src/policy.js +3 -0
- package/benchmarks/deep-logic-v1/project/src/scheduler.js +3 -0
- package/benchmarks/deep-logic-v1/project/src/stream.js +3 -0
- package/benchmarks/deep-logic-v1/project/test/smoke.test.js +20 -0
- package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +13 -0
- package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +13 -0
- package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +14 -0
- package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +13 -0
- package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +14 -0
- package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +13 -0
- package/benchmarks/deep-logic-v1/suite.json +95 -0
- package/benchmarks/deep-logic-v1/variant.mjs +28 -0
- package/docs/command-reference-vietnamese.md +5 -2
- package/docs/distribution-standard.md +7 -7
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +7 -0
- package/docs/quality-benchmark.md +49 -10
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +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/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/index.html +1 -1
- 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 +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +1 -1
- 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 +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +2 -2
- package/evals/fixtures/benchmark-assurance-evidence.valid.json +2 -2
- package/evals/fixtures/piagent-webui/runtime-command-v1.invalid.json +12 -0
- package/evals/fixtures/piagent-webui/runtime-command-v1.valid.json +11 -0
- package/evals/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +1 -1
- package/evals/fs5-pilot-protocol.v1.json +1 -1
- package/evals/fs5-pilot-protocol.v4.json +1 -1
- package/evals/fs5-pilot-protocol.v5.json +1 -1
- package/evals/private-holdout-v1/public-exposure.v1.json +3 -3
- package/evals/real-task-taxonomy.v1.json +5 -1
- package/package.json +3 -1
- package/packages/piagent-core/benchmark/benchmark-bootstrap.js +3 -2
- package/packages/piagent-core/benchmark/benchmark-cli.js +5 -1
- package/packages/piagent-core/benchmark/benchmark-matrix.js +1 -1
- package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +1 -0
- package/packages/piagent-core/extensions/acceptance-boundary-guidance.js +64 -0
- package/packages/piagent-core/extensions/acceptance-contract-semantics.js +3 -2
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -4
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +2 -2
- package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +89 -0
- package/packages/piagent-core/runtime/workflows/webui-workflow.ts +51 -0
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CbUK4gg2.js → MarkdownMessage-rD9WJKxs.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +174 -0
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +10 -0
- package/packages/piagent-webui/gateway/rpc-ui-context.ts +28 -0
- package/packages/piagent-webui/gateway/runtime-command-controller.ts +126 -0
- package/packages/piagent-webui/gateway/runtime-session-controls.ts +29 -0
- package/packages/piagent-webui/gateway/session-command-controller.ts +23 -7
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +22 -1
- package/packages/piagent-webui/gateway/session-operation-start.ts +34 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +24 -42
- package/packages/piagent-webui/package.json +2 -1
- package/packages/piagent-webui/server/loopback-server.ts +17 -0
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/runtime-command-v1.schema.json +70 -0
- package/schemas/piagent-webui/session-command-v1.schema.json +8 -0
- package/packages/piagent-webui/dist/client/assets/index-CNbYWd-R.js +0 -174
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
|
|
8
8
|
<meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
|
|
9
9
|
<title>Piagent WebUI</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-BZRFw1u6.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-BV-_03yl.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -23,6 +23,7 @@ import { SessionLeaseStore } from "./session-lease-store.ts";
|
|
|
23
23
|
import { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
|
|
24
24
|
import { SessionCommandStore } from "./session-command-store.ts";
|
|
25
25
|
import { SessionCommandController } from "./session-command-controller.ts";
|
|
26
|
+
import { RuntimeCommandController } from "./runtime-command-controller.ts";
|
|
26
27
|
import { GatewayEventStore } from "./gateway-events.ts";
|
|
27
28
|
import { SessionInspectionRegistry } from "./session-inspection-registry.ts";
|
|
28
29
|
import { SessionAttachmentRegistry } from "./session-attachment-registry.ts";
|
|
@@ -175,6 +176,7 @@ export async function startPiagentGateway(options: {
|
|
|
175
176
|
if (!attachments) throw new Error("session-attachment-unavailable");
|
|
176
177
|
return attachments.reserveForPrompt(sessionRef, refs, messageRequestId, text);
|
|
177
178
|
} });
|
|
179
|
+
const runtimeCommands = new RuntimeCommandController({ catalog: readCatalog, runtimes, events });
|
|
178
180
|
const protocol = new GatewayProtocolService({ capabilities: () => capabilities(gatewayInstanceRef, true), catalog: readCatalog,
|
|
179
181
|
events, command: commands });
|
|
180
182
|
const inspections = new SessionInspectionRegistry({ gatewayInstanceRef, host, key, packageRoot: options.packageRoot, agentDir: state.agentDir,
|
|
@@ -194,6 +196,7 @@ export async function startPiagentGateway(options: {
|
|
|
194
196
|
readSessionModel: (sessionRef) => inspections.provider(sessionRef),
|
|
195
197
|
readSessionConnections: (sessionRef) => inspections.connections(sessionRef),
|
|
196
198
|
executeSessionConnection: (command) => inspections.executeConnectionCommand(command),
|
|
199
|
+
executeRuntimeCommand: (command) => runtimeCommands.execute(command),
|
|
197
200
|
executeSessionAttachment: (sessionRef, command) => attachments.execute(sessionRef, command),
|
|
198
201
|
executeApproval: (approvalRef, decision) => runtimes!.decideApproval(approvalRef, decision),
|
|
199
202
|
readMcpAuthJob: (jobRef) => mcpAuth!.read(jobRef),
|
|
@@ -31,6 +31,8 @@ export class GatewaySessionStream {
|
|
|
31
31
|
#truncated = false;
|
|
32
32
|
#started = false;
|
|
33
33
|
#startListeners: Array<() => void> = [];
|
|
34
|
+
#settled = false;
|
|
35
|
+
#settleListeners: Array<() => void> = [];
|
|
34
36
|
readonly #toolRefs = new Map<string, string>();
|
|
35
37
|
|
|
36
38
|
constructor(options: { sessionRef: string; operationRef: string; events: GatewayEventStore }) {
|
|
@@ -42,10 +44,18 @@ export class GatewaySessionStream {
|
|
|
42
44
|
return new Promise((resolve) => this.#startListeners.push(resolve));
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
settled(): Promise<void> {
|
|
48
|
+
if (this.#settled) return Promise.resolve();
|
|
49
|
+
return new Promise((resolve) => this.#settleListeners.push(resolve));
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
observe(event: any): void {
|
|
46
53
|
if (event?.type === "agent_start") {
|
|
47
54
|
this.#started = true; for (const resolve of this.#startListeners.splice(0)) resolve(); return;
|
|
48
55
|
}
|
|
56
|
+
if (event?.type === "agent_settled") {
|
|
57
|
+
this.#settled = true; for (const resolve of this.#settleListeners.splice(0)) resolve(); return;
|
|
58
|
+
}
|
|
49
59
|
if (event?.type === "message_start" && event.message?.role === "assistant") {
|
|
50
60
|
this.#messageRef = ref("message"); this.#buffer = ""; return;
|
|
51
61
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GATEWAY_RUNTIME_UI_MARKER } from "../ownership/gateway-runtime-context.ts";
|
|
2
|
+
|
|
3
|
+
export function rpcUiContext(): object {
|
|
4
|
+
const plain = (text: unknown): string => String(text ?? "");
|
|
5
|
+
const theme = Object.freeze({
|
|
6
|
+
fg: (_color: unknown, text: unknown) => plain(text),
|
|
7
|
+
bg: (_color: unknown, text: unknown) => plain(text),
|
|
8
|
+
bold: plain,
|
|
9
|
+
italic: plain,
|
|
10
|
+
underline: plain,
|
|
11
|
+
inverse: plain,
|
|
12
|
+
strikethrough: plain,
|
|
13
|
+
getFgAnsi: () => "",
|
|
14
|
+
getBgAnsi: () => "",
|
|
15
|
+
getColorMode: () => "truecolor",
|
|
16
|
+
getThinkingBorderColor: () => plain,
|
|
17
|
+
getBashModeBorderColor: () => plain
|
|
18
|
+
});
|
|
19
|
+
return new Proxy({}, {
|
|
20
|
+
get(_target, property) {
|
|
21
|
+
if (property === GATEWAY_RUNTIME_UI_MARKER) return true;
|
|
22
|
+
if (property === "theme") return theme;
|
|
23
|
+
if (property === "confirm") return () => new Promise<boolean>(() => undefined);
|
|
24
|
+
if (property === "select" || property === "input") return async () => undefined;
|
|
25
|
+
return () => undefined;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
WEBUI_RUNTIME_ACTIONS,
|
|
5
|
+
buildWebUiRuntimeCommand,
|
|
6
|
+
isWebUiRuntimeActionId
|
|
7
|
+
} from "../../piagent-core/runtime/workflows/webui-runtime-command.ts";
|
|
8
|
+
import type { Catalog } from "../contracts/generated/session-catalog-v1.ts";
|
|
9
|
+
import type { Action, Command, Receipt } from "../contracts/generated/runtime-command-v1.ts";
|
|
10
|
+
import { GatewayEventStore } from "./gateway-events.ts";
|
|
11
|
+
import { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
|
|
12
|
+
|
|
13
|
+
const REF = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
|
|
14
|
+
const REVISION = /^[A-Za-z0-9][A-Za-z0-9._:~-]{0,159}$/;
|
|
15
|
+
const MAX_REPLAYS = 200;
|
|
16
|
+
|
|
17
|
+
type Replay = { digest: string; receipt: Receipt };
|
|
18
|
+
|
|
19
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseCommand(value: unknown): Command | null {
|
|
24
|
+
if (!record(value)) return null;
|
|
25
|
+
const expected = ["schemaVersion", "version", "messageType", "requestId", "sessionRef", "expectedSessionRevision", "action", "argument", "confirmed"];
|
|
26
|
+
const keys = Object.keys(value);
|
|
27
|
+
if (keys.length !== expected.length || !keys.every((key) => expected.includes(key))) return null;
|
|
28
|
+
if (value.schemaVersion !== 1 || value.version !== "piagent-runtime-command-v1" || value.messageType !== "command"
|
|
29
|
+
|| typeof value.requestId !== "string" || !REF.test(value.requestId)
|
|
30
|
+
|| typeof value.sessionRef !== "string" || !REF.test(value.sessionRef)
|
|
31
|
+
|| typeof value.expectedSessionRevision !== "string" || !REVISION.test(value.expectedSessionRevision)
|
|
32
|
+
|| !isWebUiRuntimeActionId(value.action)
|
|
33
|
+
|| !(value.argument === null || typeof value.argument === "string" && value.argument.length <= 2_048 && !value.argument.includes("\0"))
|
|
34
|
+
|| typeof value.confirmed !== "boolean") return null;
|
|
35
|
+
return value as unknown as Command;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function commandDigest(command: Command): string {
|
|
39
|
+
return createHash("sha256").update(JSON.stringify({ sessionRef: command.sessionRef,
|
|
40
|
+
expectedSessionRevision: command.expectedSessionRevision, action: command.action,
|
|
41
|
+
argument: command.argument, confirmed: command.confirmed })).digest("hex");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class RuntimeCommandController {
|
|
45
|
+
readonly #catalog: () => Promise<Catalog>;
|
|
46
|
+
readonly #runtimes: SessionRuntimeSupervisor;
|
|
47
|
+
readonly #events: GatewayEventStore;
|
|
48
|
+
readonly #replays = new Map<string, Replay>();
|
|
49
|
+
readonly #chains = new Map<string, Promise<unknown>>();
|
|
50
|
+
|
|
51
|
+
constructor(options: { catalog(): Promise<Catalog>; runtimes: SessionRuntimeSupervisor; events: GatewayEventStore }) {
|
|
52
|
+
this.#catalog = options.catalog;
|
|
53
|
+
this.#runtimes = options.runtimes;
|
|
54
|
+
this.#events = options.events;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
execute(input: unknown): Promise<Receipt> {
|
|
58
|
+
const command = parseCommand(input);
|
|
59
|
+
if (!command) return Promise.reject(new Error("runtime-command-invalid"));
|
|
60
|
+
const prior = this.#chains.get(command.sessionRef) ?? Promise.resolve();
|
|
61
|
+
const run = prior.then(() => this.#execute(command), () => this.#execute(command));
|
|
62
|
+
const tail = run.catch(() => undefined);
|
|
63
|
+
this.#chains.set(command.sessionRef, tail);
|
|
64
|
+
return run.finally(() => { if (this.#chains.get(command.sessionRef) === tail) this.#chains.delete(command.sessionRef); });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async #execute(command: Command): Promise<Receipt> {
|
|
68
|
+
const digest = commandDigest(command), replay = this.#replays.get(command.requestId);
|
|
69
|
+
if (replay) {
|
|
70
|
+
if (replay.digest !== digest) return this.#receipt(command, "rejected", "invalid-command", "idempotency-payload-mismatch");
|
|
71
|
+
return replay.receipt;
|
|
72
|
+
}
|
|
73
|
+
const catalog = await this.#readyCatalog();
|
|
74
|
+
const row = catalog.sessions.find((session) => session.sessionRef === command.sessionRef);
|
|
75
|
+
if (!row || row.archived) return this.#settle(command, digest,
|
|
76
|
+
this.#receipt(command, "rejected", "unavailable", row ? "session-archived" : "session-not-found"));
|
|
77
|
+
if (row.sessionRevision !== command.expectedSessionRevision) return this.#settle(command, digest,
|
|
78
|
+
this.#receipt(command, "rejected", "stale-revision", "session-revision-stale", row.sessionRevision));
|
|
79
|
+
|
|
80
|
+
let built: ReturnType<typeof buildWebUiRuntimeCommand>;
|
|
81
|
+
try { built = buildWebUiRuntimeCommand({ action: command.action, argument: command.argument, confirmed: command.confirmed }); }
|
|
82
|
+
catch (error) {
|
|
83
|
+
const reason = error instanceof Error ? error.message : "runtime-command-invalid";
|
|
84
|
+
return this.#settle(command, digest, this.#receipt(command, "rejected",
|
|
85
|
+
reason === "runtime-command-confirmation-required" ? "confirmation-required" : "invalid-command", reason, row.sessionRevision));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const result = await this.#runtimes.runRuntimeCommand(command.sessionRef, built.command);
|
|
90
|
+
const after = await this.#readyCatalog();
|
|
91
|
+
const afterRow = after.sessions.find((session) => session.sessionRef === command.sessionRef);
|
|
92
|
+
const zeroTurnViolation = built.spec.effect === "read-only" && result.modelCallObserved;
|
|
93
|
+
const receipt = this.#receipt(command, zeroTurnViolation ? "uncertain" : "settled",
|
|
94
|
+
zeroTurnViolation ? "effect-unknown" : "completed",
|
|
95
|
+
zeroTurnViolation ? "read-only-command-started-model-call" : null,
|
|
96
|
+
afterRow?.sessionRevision ?? null, result.modelCallObserved, result.outputs as Receipt["outputs"]);
|
|
97
|
+
this.#events.publish("session.changed", { catalogRevision: after.catalogRevision, session: afterRow ?? row });
|
|
98
|
+
return this.#settle(command, digest, receipt);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const reason = error instanceof Error ? error.message : "runtime-command-unavailable";
|
|
101
|
+
const resultCode = reason.includes("busy") ? "runtime-busy" : "unavailable";
|
|
102
|
+
return this.#settle(command, digest,
|
|
103
|
+
this.#receipt(command, "rejected", resultCode, reason, row.sessionRevision));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async #readyCatalog(): Promise<Catalog> {
|
|
108
|
+
const catalog = await this.#catalog();
|
|
109
|
+
if (catalog.state !== "ready" || !catalog.catalogRevision) throw new Error(catalog.reasonCode ?? "catalog-unavailable");
|
|
110
|
+
return catalog;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#receipt(command: Command, state: Receipt["state"], resultCode: Receipt["resultCode"], reasonCode: string | null,
|
|
114
|
+
sessionRevisionAfter: string | null = null, modelCallObserved = false, outputs: Receipt["outputs"] = []): Receipt {
|
|
115
|
+
const spec = WEBUI_RUNTIME_ACTIONS.find((item) => item.id === command.action)!;
|
|
116
|
+
return { schemaVersion: 1, version: "piagent-runtime-receipt-v1", messageType: "receipt",
|
|
117
|
+
requestId: command.requestId, sessionRef: command.sessionRef, action: command.action as Action,
|
|
118
|
+
state, resultCode, effect: spec.effect, modelCallObserved, outputs, sessionRevisionAfter, reasonCode };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#settle(command: Command, digest: string, receipt: Receipt): Receipt {
|
|
122
|
+
this.#replays.set(command.requestId, { digest, receipt });
|
|
123
|
+
while (this.#replays.size > MAX_REPLAYS) this.#replays.delete(this.#replays.keys().next().value as string);
|
|
124
|
+
return receipt;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
|
|
2
|
+
|
|
3
|
+
export async function executePermissionCommand(session: any,
|
|
4
|
+
permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<void> {
|
|
5
|
+
const before = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
6
|
+
await session.prompt(`/permission ${permissionMode}`);
|
|
7
|
+
const messages = Array.isArray(session.messages) ? session.messages.slice(before) : [];
|
|
8
|
+
const observed = messages.reverse().find((message: any) => message?.role === "custom"
|
|
9
|
+
&& message.customType === "piagent-permission-profile");
|
|
10
|
+
const profile = observed?.details?.permissionProfile;
|
|
11
|
+
if (!profile || profile.mode !== permissionMode || profile.warning) throw new Error("session-permission-unavailable");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function executeRuntimeCommand(session: any, command: string): Promise<{
|
|
15
|
+
outputs: Array<{ customType: string; content: string; truncated: boolean; redacted: boolean }>;
|
|
16
|
+
modelCallObserved: boolean;
|
|
17
|
+
}> {
|
|
18
|
+
const before = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
19
|
+
await session.prompt(command);
|
|
20
|
+
const added = Array.isArray(session.messages) ? session.messages.slice(before) : [];
|
|
21
|
+
const outputs = added.filter((message: any) => message?.role === "custom").slice(-8).map((message: any) => {
|
|
22
|
+
const source = typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? "");
|
|
23
|
+
const bounded = source.slice(0, 12_000), redacted = redactSensitiveText(bounded);
|
|
24
|
+
const rawType = String(message.customType ?? "runtime-output");
|
|
25
|
+
return { customType: /^[A-Za-z0-9._-]{1,120}$/.test(rawType) ? rawType : "runtime-output", content: redacted.text,
|
|
26
|
+
truncated: source.length > bounded.length, redacted: redacted.redacted };
|
|
27
|
+
});
|
|
28
|
+
return { outputs, modelCallObserved: added.some((message: any) => message?.role === "assistant") };
|
|
29
|
+
}
|
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import type { Catalog, SessionRow } from "../contracts/generated/session-catalog-v1.ts";
|
|
4
4
|
import type { Receipt } from "../contracts/generated/session-command-v1.ts";
|
|
5
|
+
import { buildWebUiWorkflowCommand, isWebUiWorkflowId, type WebUiWorkflowId } from "../../piagent-core/runtime/workflows/webui-workflow.ts";
|
|
5
6
|
import { GatewayEventStore } from "./gateway-events.ts";
|
|
6
7
|
import { SessionCommandStore, type SessionAction, type SessionCommandIdentity } from "./session-command-store.ts";
|
|
7
8
|
import { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
|
|
@@ -27,6 +28,10 @@ function record(value: unknown): value is Record<string, unknown> { return Boole
|
|
|
27
28
|
function exactKeys(value: Record<string, unknown>, keys: string[]): boolean {
|
|
28
29
|
const found = Object.keys(value); return found.length === keys.length && found.every((key) => keys.includes(key));
|
|
29
30
|
}
|
|
31
|
+
function requiredOptionalKeys(value: Record<string, unknown>, required: string[], optional: string[]): boolean {
|
|
32
|
+
const found = Object.keys(value), allowed = new Set([...required, ...optional]);
|
|
33
|
+
return required.every((key) => found.includes(key)) && found.every((key) => allowed.has(key));
|
|
34
|
+
}
|
|
30
35
|
function exactTimestamp(value: unknown): value is string {
|
|
31
36
|
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)
|
|
32
37
|
&& Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
@@ -35,13 +40,15 @@ function nullableRef(value: unknown): value is string | null { return value ===
|
|
|
35
40
|
|
|
36
41
|
function validPayload(action: SessionAction, payload: Record<string, unknown>): boolean {
|
|
37
42
|
if (["session.archive", "session.unarchive", "session.acquire", "session.release"].includes(action)) return exactKeys(payload, []);
|
|
38
|
-
if (action === "session.send") return
|
|
43
|
+
if (action === "session.send") return requiredOptionalKeys(payload,
|
|
44
|
+
["delivery", "message", "messageRequestId", "expectedOperationRef", "attachmentRefs"], ["workflow"])
|
|
39
45
|
&& Array.isArray(payload.attachmentRefs) && payload.attachmentRefs.length <= 4
|
|
40
46
|
&& new Set(payload.attachmentRefs as unknown[]).size === payload.attachmentRefs.length
|
|
41
47
|
&& (payload.attachmentRefs as unknown[]).every((item) => typeof item === "string" && REF.test(item))
|
|
42
48
|
&& ["new-operation", "follow-up", "steer"].includes(String(payload.delivery)) && typeof payload.message === "string"
|
|
43
49
|
&& payload.message.length >= 1 && payload.message.length <= 32_768 && !payload.message.includes("\0") && REF.test(String(payload.messageRequestId))
|
|
44
|
-
&& nullableRef(payload.expectedOperationRef) && (payload.delivery === "new-operation" ? payload.expectedOperationRef === null : payload.expectedOperationRef !== null)
|
|
50
|
+
&& nullableRef(payload.expectedOperationRef) && (payload.delivery === "new-operation" ? payload.expectedOperationRef === null : payload.expectedOperationRef !== null)
|
|
51
|
+
&& (payload.workflow === undefined || payload.delivery === "new-operation" && isWebUiWorkflowId(payload.workflow));
|
|
45
52
|
if (action === "session.abort") return exactKeys(payload, ["operationRef", "clearQueued"])
|
|
46
53
|
&& REF.test(String(payload.operationRef)) && typeof payload.clearQueued === "boolean";
|
|
47
54
|
if (action === "session.set-model") return exactKeys(payload, ["modelRef"]) && REF.test(String(payload.modelRef));
|
|
@@ -56,11 +63,13 @@ function validPayload(action: SessionAction, payload: Record<string, unknown>):
|
|
|
56
63
|
&& (payload.title === null || typeof payload.title === "string" && payload.title.length >= 1 && payload.title.length <= 500
|
|
57
64
|
&& !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(payload.title));
|
|
58
65
|
const createKeys = ["projectRef", "placeRef", "modelRef", "thinkingLevel", "message", "messageRequestId"];
|
|
59
|
-
return (
|
|
66
|
+
return requiredOptionalKeys(payload, createKeys, ["deferInitialMessage", "permissionMode", "workflow"])
|
|
60
67
|
&& REF.test(String(payload.projectRef)) && REF.test(String(payload.placeRef)) && nullableRef(payload.modelRef)
|
|
61
68
|
&& ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(payload.thinkingLevel))
|
|
62
69
|
&& typeof payload.message === "string" && payload.message.length >= 1 && payload.message.length <= 32_768 && !payload.message.includes("\0")
|
|
63
|
-
&& REF.test(String(payload.messageRequestId)) && (payload.deferInitialMessage === undefined || typeof payload.deferInitialMessage === "boolean")
|
|
70
|
+
&& REF.test(String(payload.messageRequestId)) && (payload.deferInitialMessage === undefined || typeof payload.deferInitialMessage === "boolean")
|
|
71
|
+
&& (payload.permissionMode === undefined || ["read-only", "workspace-write", "trusted-full-access"].includes(String(payload.permissionMode)))
|
|
72
|
+
&& (payload.workflow === undefined || isWebUiWorkflowId(payload.workflow));
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
function parseCommand(value: unknown): SessionCommand | null {
|
|
@@ -147,11 +156,17 @@ export class SessionCommandController {
|
|
|
147
156
|
if (command.action === "session.create") {
|
|
148
157
|
targetSessionRef = await this.#runtimes.create(String(command.payload.projectRef), String(command.payload.placeRef),
|
|
149
158
|
command.payload.modelRef === null ? null : String(command.payload.modelRef), String(command.payload.thinkingLevel));
|
|
159
|
+
if (command.payload.permissionMode !== undefined) {
|
|
160
|
+
await this.#runtimes.setPermission(targetSessionRef, String(command.payload.permissionMode) as
|
|
161
|
+
"read-only" | "workspace-write" | "trusted-full-access");
|
|
162
|
+
}
|
|
150
163
|
const created = await this.#readyCatalog(), createdRow = created.sessions.find((item) => item.sessionRef === targetSessionRef);
|
|
151
164
|
if (!createdRow) throw new Error("session-catalog-refresh-failed");
|
|
152
165
|
if (command.payload.deferInitialMessage === true) resultCode = "created";
|
|
153
166
|
else {
|
|
154
|
-
const
|
|
167
|
+
const message = buildWebUiWorkflowCommand((command.payload.workflow as WebUiWorkflowId | undefined) ?? null,
|
|
168
|
+
String(command.payload.message));
|
|
169
|
+
const sent = await this.#runtimes.send(targetSessionRef, { delivery: "new-operation", message,
|
|
155
170
|
expectedOperationRef: null }, createdRow.sessionRevision);
|
|
156
171
|
resultCode = sent.resultCode === "started" ? "started" : sent.resultCode; operationRef = sent.operationRef;
|
|
157
172
|
}
|
|
@@ -162,7 +177,7 @@ export class SessionCommandController {
|
|
|
162
177
|
const acquired = await this.#readyCatalog(), acquiredRow = acquired.sessions.find((item) => item.sessionRef === command.sessionRef);
|
|
163
178
|
if (!acquiredRow) throw new Error("session-catalog-refresh-failed");
|
|
164
179
|
const payload = command.payload as { delivery: "new-operation" | "follow-up" | "steer"; message: string;
|
|
165
|
-
expectedOperationRef: string | null; messageRequestId: string; attachmentRefs: string[] };
|
|
180
|
+
expectedOperationRef: string | null; messageRequestId: string; attachmentRefs: string[]; workflow?: WebUiWorkflowId };
|
|
166
181
|
// Staged refs are claimed once, here, before the runtime is asked to
|
|
167
182
|
// dispatch. Documents were turned into text at staging so they join the
|
|
168
183
|
// message; images travel on the host's own channel.
|
|
@@ -172,7 +187,8 @@ export class SessionCommandController {
|
|
|
172
187
|
attachmentReservation = prepared;
|
|
173
188
|
if (!this.#prepareAttachments && payload.attachmentRefs.length > 0) throw new Error("session-attachment-unavailable");
|
|
174
189
|
sendAttempted = true;
|
|
175
|
-
const
|
|
190
|
+
const message = buildWebUiWorkflowCommand(payload.workflow ?? null, prepared.text);
|
|
191
|
+
const sent = await this.#runtimes.send(command.sessionRef!, { ...payload, message, images: prepared.images },
|
|
176
192
|
acquiredRow.sessionRevision);
|
|
177
193
|
prepared.commit();
|
|
178
194
|
resultCode = sent.resultCode; operationRef = sent.operationRef;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
4
5
|
import { readContextTelemetry } from "../../piagent-core/extensions/context-engine.js";
|
|
@@ -9,6 +10,8 @@ import { webUiModelRef } from "../../piagent-core/runtime/inspection/webui-snaps
|
|
|
9
10
|
import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
|
|
10
11
|
import { piApprovalBroker } from "../../piagent-core/runtime/inspection/approval-broker.ts";
|
|
11
12
|
import { attachmentCapability } from "../../piagent-core/runtime/input/attachment-store.ts";
|
|
13
|
+
import { WEBUI_WORKFLOW_OPTIONS } from "../../piagent-core/runtime/workflows/webui-workflow.ts";
|
|
14
|
+
import { WEBUI_RUNTIME_ACTIONS } from "../../piagent-core/runtime/workflows/webui-runtime-command.ts";
|
|
12
15
|
import { CoreInspectionProvider } from "../server/core-inspection-provider.ts";
|
|
13
16
|
import { ReadModelNotFound, type WebUiReadModelProvider } from "../server/read-model-provider.ts";
|
|
14
17
|
import { isUserConversationSession, projectRefForCwd, sessionRefForPath, type PiSessionInfo } from "./session-catalog.ts";
|
|
@@ -189,6 +192,16 @@ export class SessionInspectionRegistry {
|
|
|
189
192
|
if (!projects.has(project.projectRef)) projects.set(project.projectRef, project);
|
|
190
193
|
}
|
|
191
194
|
const availableModels = (this.#models?.getAvailableSnapshot() ?? []).slice(0, 300);
|
|
195
|
+
const profiles = safeRead(() => fs.readdirSync(path.join(this.#packageRoot, "adapters")).flatMap((entry) => {
|
|
196
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(entry)) return [];
|
|
197
|
+
const file = path.join(this.#packageRoot, "adapters", entry, "profile.json");
|
|
198
|
+
const stat = fs.lstatSync(file);
|
|
199
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_048_576) return [];
|
|
200
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
|
|
201
|
+
return [{ id: entry, displayName: safeName(value.displayName ?? entry),
|
|
202
|
+
permissionMode: ["read-only", "workspace-write", "trusted-full-access"].includes(String(value.permissionProfile))
|
|
203
|
+
? String(value.permissionProfile) : null }];
|
|
204
|
+
}).slice(0, 100), []);
|
|
192
205
|
const models = availableModels.flatMap((value) => {
|
|
193
206
|
const provider = safeModelId(value.provider);
|
|
194
207
|
const modelId = safeModelId(value.id);
|
|
@@ -201,8 +214,16 @@ export class SessionInspectionRegistry {
|
|
|
201
214
|
return [{ modelRef: webUiModelRef(provider, modelId), provider, modelId, displayName: safeName(value.name ?? modelId),
|
|
202
215
|
reasoning: value.reasoning, imageInput: inputs ? inputs.includes("image") : null, thinkingLevels }];
|
|
203
216
|
});
|
|
217
|
+
// Keep the WebUI new-session baseline deterministic and visible. If the
|
|
218
|
+
// preferred model is not authenticated, the host's own default remains the
|
|
219
|
+
// safe fallback instead of silently substituting another catalog entry.
|
|
220
|
+
const defaultModel = models.find((value) => value.provider === "openai-codex" && value.modelId === "gpt-5.6-sol");
|
|
204
221
|
return { schemaVersion: 1, version: "piagent-session-creation-options-v1", generatedAt: new Date().toISOString(),
|
|
205
|
-
projects: [...projects.values()].slice(0, 200), models,
|
|
222
|
+
projects: [...projects.values()].slice(0, 200), models,
|
|
223
|
+
defaultModelRef: defaultModel?.modelRef ?? null, defaultThinkingLevel: defaultModel ? "high" : null,
|
|
224
|
+
profiles, workflows: WEBUI_WORKFLOW_OPTIONS,
|
|
225
|
+
runtimeActions: WEBUI_RUNTIME_ACTIONS,
|
|
226
|
+
webSearch: inspectWebSearchCapability({ agentDir: this.#agentDir, models: availableModels }),
|
|
206
227
|
projectImport: nativeProjectPickerAvailable() ? { status: "available", reasonCode: null }
|
|
207
228
|
: { status: "unavailable", reasonCode: "native-project-picker-unavailable" },
|
|
208
229
|
reasonCode: projects.size ? null : "no-known-project" };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { GatewaySessionStream } from "./gateway-session-stream.ts";
|
|
2
|
+
|
|
3
|
+
const DEFERRED_OPERATION_START_GRACE_MS = 5_000;
|
|
4
|
+
|
|
5
|
+
export async function waitForOperationStart(
|
|
6
|
+
stream: GatewaySessionStream,
|
|
7
|
+
prompt: Promise<void>
|
|
8
|
+
): Promise<"prompt-owned" | "deferred"> {
|
|
9
|
+
const started = stream.started();
|
|
10
|
+
const first = await Promise.race([
|
|
11
|
+
started.then(() => ({ state: "started" as const })),
|
|
12
|
+
prompt.then(() => ({ state: "prompt-resolved" as const }),
|
|
13
|
+
(error: unknown) => ({ state: "prompt-rejected" as const, error }))
|
|
14
|
+
]);
|
|
15
|
+
if (first.state === "started") return "prompt-owned";
|
|
16
|
+
if (first.state === "prompt-rejected") throw first.error;
|
|
17
|
+
|
|
18
|
+
// Extension commands such as `/workflow` launch their agent turn through
|
|
19
|
+
// `pi.sendUserMessage()`. The outer prompt resolves before that deferred turn
|
|
20
|
+
// emits `agent_start`, so retain ownership until the real operation appears.
|
|
21
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
22
|
+
try {
|
|
23
|
+
await Promise.race([
|
|
24
|
+
started,
|
|
25
|
+
new Promise<never>((_resolve, reject) => {
|
|
26
|
+
timer = setTimeout(() => reject(new Error("session-operation-start-unobserved")),
|
|
27
|
+
DEFERRED_OPERATION_START_GRACE_MS);
|
|
28
|
+
})
|
|
29
|
+
]);
|
|
30
|
+
return "deferred";
|
|
31
|
+
} finally {
|
|
32
|
+
if (timer) clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -11,8 +11,10 @@ import { isUserConversationSession, projectRefForCwd, sessionRefForPath } from "
|
|
|
11
11
|
import { SessionLeaseStore, type SessionLeaseSnapshot } from "./session-lease-store.ts";
|
|
12
12
|
import { GatewayEventStore } from "./gateway-events.ts";
|
|
13
13
|
import { GatewaySessionStream } from "./gateway-session-stream.ts";
|
|
14
|
-
import { GATEWAY_RUNTIME_UI_MARKER } from "../ownership/gateway-runtime-context.ts";
|
|
15
14
|
import { preferAuthoritativePiagentGuard } from "./extension-authority.ts";
|
|
15
|
+
import { executePermissionCommand, executeRuntimeCommand } from "./runtime-session-controls.ts";
|
|
16
|
+
import { rpcUiContext } from "./rpc-ui-context.ts";
|
|
17
|
+
import { waitForOperationStart } from "./session-operation-start.ts";
|
|
16
18
|
|
|
17
19
|
const MAX_WARM_RUNTIMES = 10;
|
|
18
20
|
|
|
@@ -22,33 +24,7 @@ type ActiveRuntime = { runtime: RuntimeHandle; lease: SessionLeaseSnapshot; info
|
|
|
22
24
|
unsubscribe: (() => void) | null; completion: Promise<void> | null; settling: boolean; approvalWaiting: boolean;
|
|
23
25
|
unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null; sessionManager: any | null };
|
|
24
26
|
type Projection = { sessionRevision: string; liveState: "offline" | "idle" | "running" | "paused" | "waiting-approval" | "uncertain" };
|
|
25
|
-
|
|
26
|
-
function rpcUiContext(): object {
|
|
27
|
-
const plain = (text: unknown): string => String(text ?? "");
|
|
28
|
-
const theme = Object.freeze({
|
|
29
|
-
fg: (_color: unknown, text: unknown) => plain(text),
|
|
30
|
-
bg: (_color: unknown, text: unknown) => plain(text),
|
|
31
|
-
bold: plain,
|
|
32
|
-
italic: plain,
|
|
33
|
-
underline: plain,
|
|
34
|
-
inverse: plain,
|
|
35
|
-
strikethrough: plain,
|
|
36
|
-
getFgAnsi: () => "",
|
|
37
|
-
getBgAnsi: () => "",
|
|
38
|
-
getColorMode: () => "truecolor",
|
|
39
|
-
getThinkingBorderColor: () => plain,
|
|
40
|
-
getBashModeBorderColor: () => plain
|
|
41
|
-
});
|
|
42
|
-
return new Proxy({}, {
|
|
43
|
-
get(_target, property) {
|
|
44
|
-
if (property === GATEWAY_RUNTIME_UI_MARKER) return true;
|
|
45
|
-
if (property === "theme") return theme;
|
|
46
|
-
if (property === "confirm") return () => new Promise<boolean>(() => undefined);
|
|
47
|
-
if (property === "select" || property === "input") return async () => undefined;
|
|
48
|
-
return () => undefined;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
27
|
+
type RuntimeCommandResult = Awaited<ReturnType<typeof executeRuntimeCommand>>;
|
|
52
28
|
|
|
53
29
|
function productionRuntimeFactory(options: { host: any; agentDir: string; packageRoot: string; modelRuntime?: any }): RuntimeFactory {
|
|
54
30
|
return async (info, _runtimeInstanceRef, sessionManager) => {
|
|
@@ -303,12 +279,18 @@ export class SessionRuntimeSupervisor {
|
|
|
303
279
|
let prompt: Promise<void>;
|
|
304
280
|
try { prompt = session.prompt(payload.message, payload.images?.length ? { images: payload.images } : undefined); }
|
|
305
281
|
catch (error) { await this.#finishOperation(sessionRef, operationRef, stream); throw error; }
|
|
306
|
-
|
|
282
|
+
const started = waitForOperationStart(stream, prompt);
|
|
283
|
+
// A normal prompt resolves after the agent settles. A workflow extension
|
|
284
|
+
// command resolves before its nested agent turn starts. In both cases the
|
|
285
|
+
// canonical completion boundary is `agent_settled`, not the outer promise.
|
|
286
|
+
const lifecycle = started.then(async (mode) => {
|
|
287
|
+
if (mode === "deferred") await stream.settled();
|
|
288
|
+
else await Promise.race([stream.settled(), prompt]);
|
|
289
|
+
});
|
|
290
|
+
active.completion = lifecycle.then(() => this.#finishOperation(sessionRef, operationRef, stream),
|
|
307
291
|
() => this.#finishOperation(sessionRef, operationRef, stream));
|
|
308
|
-
await
|
|
309
|
-
|
|
310
|
-
prompt.then(() => { throw new Error("session-operation-start-unobserved"); }, (error: unknown) => { throw error; })
|
|
311
|
-
]);
|
|
292
|
+
try { await started; }
|
|
293
|
+
catch (error) { await active.completion; throw error; }
|
|
312
294
|
return { resultCode: "started", operationRef };
|
|
313
295
|
}
|
|
314
296
|
|
|
@@ -389,23 +371,23 @@ export class SessionRuntimeSupervisor {
|
|
|
389
371
|
return "thinking-changed";
|
|
390
372
|
}
|
|
391
373
|
|
|
392
|
-
async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"):
|
|
393
|
-
Promise<"permission-changed"> {
|
|
374
|
+
async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<"permission-changed"> {
|
|
394
375
|
await this.acquire(sessionRef);
|
|
395
376
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
396
377
|
if (!active || !session) throw new Error("session-runtime-unavailable");
|
|
397
378
|
if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
|
|
398
|
-
|
|
399
|
-
await session.prompt(`/permission ${permissionMode}`);
|
|
400
|
-
const messages = Array.isArray(session.messages) ? session.messages.slice(before) : [];
|
|
401
|
-
const observed = messages.reverse().find((message: any) => message?.role === "custom"
|
|
402
|
-
&& message.customType === "piagent-permission-profile");
|
|
403
|
-
const profile = observed?.details?.permissionProfile;
|
|
404
|
-
if (!profile || profile.mode !== permissionMode || profile.warning) throw new Error("session-permission-unavailable");
|
|
379
|
+
await executePermissionCommand(session, permissionMode);
|
|
405
380
|
this.#touchCreated(sessionRef);
|
|
406
381
|
return "permission-changed";
|
|
407
382
|
}
|
|
408
383
|
|
|
384
|
+
async runRuntimeCommand(sessionRef: string, command: string): Promise<RuntimeCommandResult> {
|
|
385
|
+
await this.acquire(sessionRef);
|
|
386
|
+
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
387
|
+
if (!active || !session) throw new Error("session-runtime-unavailable");
|
|
388
|
+
if (active.operationRef || active.settling || !session.isIdle) throw new Error("session-runtime-busy");
|
|
389
|
+
const result = await executeRuntimeCommand(session, command); this.#touchCreated(sessionRef); return result;
|
|
390
|
+
}
|
|
409
391
|
approvalProjection(sessionRef: string): { revision: string | null; summary: Record<string, unknown> } {
|
|
410
392
|
const active = this.#active.get(sessionRef);
|
|
411
393
|
if (!active) return { revision: null, summary: { state: "unknown", pending: [], recent: [],
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"check:contracts": "node scripts/generate-contracts.mjs --check",
|
|
9
9
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
10
10
|
"build": "npm run check:contracts && vite build",
|
|
11
|
-
"benchmark": "node benchmark/benchmark.mjs"
|
|
11
|
+
"benchmark": "node benchmark/benchmark.mjs",
|
|
12
|
+
"benchmark:parity": "node benchmark/parity-benchmark.mjs"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
15
|
"@emotion/cache": "^11.14.0",
|
|
@@ -61,6 +61,7 @@ export async function startLoopbackServer(options: {
|
|
|
61
61
|
executeSessionAttachment?: (sessionRef: string, command: unknown) => unknown | Promise<unknown>;
|
|
62
62
|
readSessionConnections?: (sessionRef: string) => unknown | Promise<unknown>;
|
|
63
63
|
executeSessionConnection?: (command: unknown) => unknown | Promise<unknown>;
|
|
64
|
+
executeRuntimeCommand?: (command: unknown) => unknown | Promise<unknown>;
|
|
64
65
|
readMcpAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
|
|
65
66
|
cancelMcpAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
|
|
66
67
|
readProviderAuthCatalog?: () => unknown | Promise<unknown>;
|
|
@@ -205,6 +206,22 @@ export async function startLoopbackServer(options: {
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
if (request.method === "POST" && url.pathname === "/api/v1/runtime-commands" && options.executeRuntimeCommand) {
|
|
210
|
+
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
211
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
212
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
213
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
214
|
+
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
215
|
+
let command: unknown;
|
|
216
|
+
try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
|
|
217
|
+
catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-runtime-command"); }
|
|
218
|
+
try { return jsonResponse(response, 200, await options.executeRuntimeCommand(command)); }
|
|
219
|
+
catch (error) {
|
|
220
|
+
const code = error instanceof Error ? error.message : "runtime-command-unavailable";
|
|
221
|
+
return errorResponse(response, code.includes("invalid") ? 400 : 503, code);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
208
225
|
if (request.method === "POST" && url.pathname.startsWith("/api/v1/mcp-auth/jobs/") && url.pathname.endsWith("/cancel") && options.cancelMcpAuthJob) {
|
|
209
226
|
if (requestOrigin !== origin) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
210
227
|
const mutationSession = auth.authorizeMutation(request);
|
|
@@ -227,6 +227,15 @@
|
|
|
227
227
|
"supported": true,
|
|
228
228
|
"current": true
|
|
229
229
|
},
|
|
230
|
+
{
|
|
231
|
+
"name": "runtime-command-v1",
|
|
232
|
+
"file": "runtime-command-v1.schema.json",
|
|
233
|
+
"id": "https://piagent.io.vn/schemas/piagent-webui/runtime-command-v1.schema.json",
|
|
234
|
+
"documentVersion": 1,
|
|
235
|
+
"role": "command",
|
|
236
|
+
"supported": true,
|
|
237
|
+
"current": true
|
|
238
|
+
},
|
|
230
239
|
{
|
|
231
240
|
"name": "gateway-capabilities-v1",
|
|
232
241
|
"file": "gateway-capabilities-v1.schema.json",
|