@piagent/platform 1.4.0 → 1.5.1
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 +151 -0
- package/README.md +12 -2
- package/README.vi.md +47 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +13 -0
- package/docs/languages.json +20 -4
- package/docs/mcp-and-tools.md +112 -1
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +42 -0
- package/docs/release-install-policy.md +21 -8
- package/docs/runtime-harness-standard.md +1 -1
- package/docs/security-threat-model.md +1 -1
- package/docs/task-implementation-contract.md +11 -4
- package/docs/team-onboarding.md +2 -2
- package/docs/usage-observability.md +70 -0
- package/docs/vi/architecture.md +13 -0
- package/docs-site/architecture.html +5 -1
- package/docs-site/benchmark.html +5 -1
- package/docs-site/commands.html +5 -1
- package/docs-site/content/en/index.html +49 -0
- package/docs-site/content/en/runtime.html +13 -0
- package/docs-site/content/index.html +49 -0
- package/docs-site/content/runtime.html +28 -0
- package/docs-site/context-engine.html +5 -1
- package/docs-site/en/architecture.html +5 -1
- package/docs-site/en/benchmark.html +5 -1
- package/docs-site/en/commands.html +5 -1
- package/docs-site/en/context-engine.html +5 -1
- package/docs-site/en/index.html +54 -1
- package/docs-site/en/mcp-approval.html +5 -1
- package/docs-site/en/mcp-auth.html +5 -1
- package/docs-site/en/mcp-servers.html +5 -1
- package/docs-site/en/mcp.html +5 -1
- package/docs-site/en/permissions.html +5 -1
- package/docs-site/en/profiles.html +5 -1
- package/docs-site/en/quickstart.html +5 -1
- package/docs-site/en/runtime.html +18 -1
- package/docs-site/en/scope.html +5 -1
- package/docs-site/en/security.html +5 -1
- package/docs-site/en/team.html +5 -1
- package/docs-site/en/whats-new.html +5 -1
- package/docs-site/en/workflows.html +5 -1
- package/docs-site/index.html +54 -1
- package/docs-site/mcp-approval.html +5 -1
- package/docs-site/mcp-auth.html +5 -1
- package/docs-site/mcp-servers.html +5 -1
- package/docs-site/mcp.html +5 -1
- package/docs-site/permissions.html +5 -1
- package/docs-site/profiles.html +5 -1
- package/docs-site/quickstart.html +5 -1
- package/docs-site/runtime.html +33 -1
- package/docs-site/scope.html +5 -1
- package/docs-site/security.html +5 -1
- package/docs-site/team.html +5 -1
- package/docs-site/whats-new.html +5 -1
- package/docs-site/workflows.html +5 -1
- package/evals/fixtures/piagent-webui/document-workspace-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/document-workspace-v1.valid.json +42 -0
- package/evals/fixtures/piagent-webui/session-command-v1.valid.json +2 -1
- package/package.json +14 -2
- package/packages/piagent-core/README.md +3 -2
- package/packages/piagent-core/capabilities/capability-verification-cache.js +17 -7
- package/packages/piagent-core/capabilities/runtime-integrity.js +51 -4
- package/packages/piagent-core/extensions/document-intake.ts +6 -3
- package/packages/piagent-core/extensions/piagent-guard.ts +20 -237
- package/packages/piagent-core/extensions/runtime-evidence.js +12 -1
- package/packages/piagent-core/extensions/shell-reach.ts +250 -0
- package/packages/piagent-core/mcp/mcp-command-actions.js +1 -1
- package/packages/piagent-core/mcp/mcp-server-catalog.js +135 -5
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +9 -2
- package/packages/piagent-core/runtime/input/attachment-document-command.ts +39 -0
- package/packages/piagent-core/runtime/input/attachment-reservation.ts +36 -0
- package/packages/{piagent-webui/extension → piagent-core/runtime/input}/attachment-store.ts +221 -44
- package/packages/piagent-core/runtime/inspection/document-workspace-projection.ts +276 -0
- package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +20 -5
- package/packages/piagent-core/runtime/inspection/session-identity.ts +20 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +25 -2
- package/packages/piagent-core/runtime/policy/block-remedy.ts +139 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +9 -13
- package/packages/piagent-core/runtime/registration/workflow-commands.ts +5 -4
- package/packages/piagent-core/runtime/session/token-economics.js +34 -0
- package/packages/piagent-core/runtime/tools/phase-tool-runtime.ts +2 -1
- package/packages/piagent-core/runtime/workflows/input-routing.ts +13 -2
- package/packages/piagent-core/runtime/workflows/task-intake.ts +5 -0
- package/packages/piagent-core/security/sensitive-data.js +7 -0
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-Uouhx1QO.js +29 -0
- package/packages/piagent-webui/dist/client/assets/index-BV-_03yl.css +1 -0
- package/packages/piagent-webui/dist/client/assets/index-MBZq8XLV.js +174 -0
- package/packages/piagent-webui/dist/client/index.html +2 -2
- package/packages/piagent-webui/extension/lifecycle-controller.ts +15 -0
- package/packages/piagent-webui/extension/piagent-webui.ts +19 -6
- package/packages/piagent-webui/extension/same-session-bridge.ts +12 -12
- package/packages/piagent-webui/gateway/ai-capability-inspection.ts +53 -0
- package/packages/piagent-webui/gateway/extension-authority.ts +47 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +23 -3
- package/packages/piagent-webui/gateway/native-project-picker.ts +63 -7
- package/packages/piagent-webui/gateway/session-attachment-registry.ts +165 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +62 -9
- package/packages/piagent-webui/gateway/session-command-controller.ts +47 -14
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +67 -12
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +24 -11
- package/packages/piagent-webui/package.json +3 -1
- package/packages/piagent-webui/server/core-inspection-provider.ts +52 -15
- package/packages/piagent-webui/server/ipc-read-model-client.ts +2 -0
- package/packages/piagent-webui/server/loopback-server.ts +63 -25
- package/packages/piagent-webui/server/read-model-provider.ts +2 -0
- package/packages/piagent-webui/server/read-only-router.ts +6 -0
- package/packages/piagent-webui/server/transcript-projection.ts +58 -2
- package/schemas/piagent-webui/attachment-v1.schema.json +24 -4
- package/schemas/piagent-webui/capabilities-v1.schema.json +2 -2
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/document-workspace-v1.schema.json +155 -0
- package/schemas/piagent-webui/session-command-v1.schema.json +14 -3
- package/schemas/piagent-webui/transcript-v1.schema.json +27 -0
- package/scripts/build-docs-site.mjs +21 -1
- package/scripts/explain-command.mjs +240 -0
- package/scripts/install-global.sh +104 -3
- package/scripts/mcp-manage.mjs +17 -4
- package/scripts/pi-session-stats.sh +10 -2
- package/scripts/piagent-cli.mjs +14 -1
- package/scripts/piagent-dashboard.mjs +55 -2
- package/scripts/setup.sh +69 -33
- package/scripts/traffic-snapshot.mjs +214 -0
- package/scripts/typescript-loader.mjs +20 -4
- package/scripts/uninstall-global.sh +53 -2
- package/scripts/update-global.mjs +33 -1
- package/scripts/verify-local.sh +20 -1
- package/scripts/verify-translation-pairing.mjs +121 -0
- package/packages/piagent-webui/dist/client/assets/index-DjMntaK5.js +0 -172
- package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +0 -1
|
@@ -22,7 +22,7 @@ type EventStore = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
async function loadCoreInspection() {
|
|
25
|
-
const [{ collectFileDiff }, { buildWebUiInspectionProjection }, { matchesProtectedPath }, reviewProjection, reviewStore, mutationProjection, revertProjection, openProjection, commitSummaryProjection, taskIndexProjection, taskTimelineProjection, recoveryHistoryProjection, handoffHistoryProjection, subagentTreeProjection, releaseMonitorProjection] = await Promise.all([
|
|
25
|
+
const [{ collectFileDiff }, { buildWebUiInspectionProjection }, { matchesProtectedPath }, reviewProjection, reviewStore, mutationProjection, revertProjection, openProjection, commitSummaryProjection, taskIndexProjection, taskTimelineProjection, recoveryHistoryProjection, handoffHistoryProjection, subagentTreeProjection, releaseMonitorProjection, documentWorkspaceProjection] = await Promise.all([
|
|
26
26
|
import(`${CORE_ROOT}/runtime/inspection/diff-projection.ts`),
|
|
27
27
|
import(`${CORE_ROOT}/runtime/inspection/webui-snapshot.ts`),
|
|
28
28
|
import(`${CORE_ROOT}/extensions/policy-core.js`),
|
|
@@ -37,9 +37,10 @@ async function loadCoreInspection() {
|
|
|
37
37
|
import(`${CORE_ROOT}/runtime/inspection/task-compaction-history.ts`),
|
|
38
38
|
import(`${CORE_ROOT}/runtime/inspection/task-handoff-history.ts`),
|
|
39
39
|
import(`${CORE_ROOT}/runtime/inspection/task-subagent-tree.ts`),
|
|
40
|
-
import("./benchmark-release-monitor.ts")
|
|
40
|
+
import("./benchmark-release-monitor.ts"),
|
|
41
|
+
import(`${CORE_ROOT}/runtime/inspection/document-workspace-projection.ts`)
|
|
41
42
|
]);
|
|
42
|
-
return { collectFileDiff, buildWebUiInspectionProjection, matchesProtectedPath, ...reviewProjection, ...reviewStore, ...mutationProjection, ...revertProjection, ...openProjection, ...commitSummaryProjection, ...taskIndexProjection, ...taskTimelineProjection, ...recoveryHistoryProjection, ...handoffHistoryProjection, ...subagentTreeProjection, ...releaseMonitorProjection };
|
|
43
|
+
return { collectFileDiff, buildWebUiInspectionProjection, matchesProtectedPath, ...reviewProjection, ...reviewStore, ...mutationProjection, ...revertProjection, ...openProjection, ...commitSummaryProjection, ...taskIndexProjection, ...taskTimelineProjection, ...recoveryHistoryProjection, ...handoffHistoryProjection, ...subagentTreeProjection, ...releaseMonitorProjection, ...documentWorkspaceProjection };
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export type CoreInspectionInput = {
|
|
@@ -52,12 +53,16 @@ export type CoreInspectionInput = {
|
|
|
52
53
|
currentActivity?: () => unknown[];
|
|
53
54
|
sessionEntries?: () => unknown[];
|
|
54
55
|
protectedPaths?: () => string[];
|
|
56
|
+
// Extra directories the operator granted through the profile. The document
|
|
57
|
+
// workspace lists them beside the project; without them it shows the project
|
|
58
|
+
// only, which is the same answer a host with no grants gives.
|
|
59
|
+
documentReadRoots?: () => unknown;
|
|
55
60
|
contextUsage?: () => { tokens: number | null; contextWindow: number; percent: number | null } | undefined;
|
|
56
61
|
model?: () => { provider?: unknown; id?: unknown; name?: unknown; reasoning?: unknown; input?: unknown; thinkingLevelMap?: unknown; contextWindow?: unknown; maxTokens?: unknown } | undefined;
|
|
57
62
|
thinkingLevel?: () => unknown;
|
|
58
63
|
queueProjection?: () => unknown;
|
|
59
64
|
modelCatalog?: () => unknown;
|
|
60
|
-
attachmentCapability?: () => { kinds: Array<"file" | "image">; mimeTypes: string[] };
|
|
65
|
+
attachmentCapability?: () => { kinds: Array<"file" | "image" | "document">; mimeTypes: string[] };
|
|
61
66
|
approvalProjection?: () => { revision: string | null; summary: Record<string, unknown> };
|
|
62
67
|
approvalDetail?: (approvalRef: string) => unknown | null;
|
|
63
68
|
sourceMutationGuardAvailable?: () => boolean;
|
|
@@ -75,6 +80,7 @@ export class CoreInspectionProvider implements WebUiReadModelProvider {
|
|
|
75
80
|
#cachedProjection: { at: number; value: any } | null = null;
|
|
76
81
|
#invalidationEpoch = 0;
|
|
77
82
|
readonly #diffCache = new Map<string, { at: number; value: unknown }>();
|
|
83
|
+
#documentCache: { at: number; value: unknown } | null = null;
|
|
78
84
|
readonly #sourceRevertConfirmationKey = randomBytes(32);
|
|
79
85
|
|
|
80
86
|
constructor(input: CoreInspectionInput) { this.#input = input; }
|
|
@@ -83,6 +89,7 @@ export class CoreInspectionProvider implements WebUiReadModelProvider {
|
|
|
83
89
|
this.#invalidationEpoch += 1;
|
|
84
90
|
this.#cachedProjection = null;
|
|
85
91
|
this.#diffCache.clear();
|
|
92
|
+
this.#documentCache = null;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
async #projection() {
|
|
@@ -97,11 +104,22 @@ export class CoreInspectionProvider implements WebUiReadModelProvider {
|
|
|
97
104
|
contextUsage: this.#input.contextUsage?.(), model: this.#input.model?.(), thinkingLevel: this.#input.thinkingLevel?.(),
|
|
98
105
|
eventCursor: this.#input.eventStore.currentCursor(), resyncRequired: this.#input.eventStore.resyncRequired(), eventReplay: retention
|
|
99
106
|
});
|
|
107
|
+
// What the host will accept as an attachment is a property of the machine and
|
|
108
|
+
// the model, not of chat control. The Gateway drives sessions without a chat
|
|
109
|
+
// bridge at all, and a capability published only alongside that bridge left
|
|
110
|
+
// its composer believing no file type was supported.
|
|
111
|
+
if (this.#input.attachmentCapability) {
|
|
112
|
+
const attachment = this.#input.attachmentCapability();
|
|
113
|
+
value.snapshot.capabilities.capabilities.attachments = { status: "available", version: 1, reason: null,
|
|
114
|
+
kinds: attachment.kinds, mimeTypes: attachment.mimeTypes };
|
|
115
|
+
Object.assign(value.snapshot.capabilities.limits, { maxRequestBodyBytes: 11_250_000, maxAttachmentCount: 4,
|
|
116
|
+
maxAttachmentFileBytes: 8_388_608, maxAttachmentTotalBytes: 16_777_216 });
|
|
117
|
+
}
|
|
100
118
|
const control = this.#input.chatControl?.();
|
|
101
119
|
if (control?.state === "ready" && control.identity && control.revisions && !this.#input.eventStore.resyncRequired()) {
|
|
102
120
|
const available = { available: true, reasonCode: null }, disabled = (reasonCode: string) => ({ available: false, reasonCode });
|
|
103
121
|
const lifecycle = this.#input.lifecycleControl?.(), terminal = control.taskState === "terminal";
|
|
104
|
-
const dispatchBlocked =
|
|
122
|
+
const dispatchBlocked = lifecycle?.dispatchBlocked === true && lifecycle.state !== "terminal";
|
|
105
123
|
value.snapshot.identity = structuredClone(control.identity);
|
|
106
124
|
value.snapshot.capabilities.identity = structuredClone(control.identity);
|
|
107
125
|
for (const name of ["runtimeRevision", "taskRevision", "controlRevision", "sessionOptionRevision", "queueRevision"])
|
|
@@ -109,17 +127,10 @@ export class CoreInspectionProvider implements WebUiReadModelProvider {
|
|
|
109
127
|
value.snapshot.capabilities.mode = "control-enabled";
|
|
110
128
|
value.snapshot.capabilities.capabilities["control.chat"] = {
|
|
111
129
|
status: "available", version: 1, reason: null, queuePersistence: "runtime-lifetime",
|
|
112
|
-
actions: { send: dispatchBlocked ? disabled(
|
|
113
|
-
deleteHeld: available, dispatchHeld: dispatchBlocked ? disabled(
|
|
114
|
-
interruptAndSend: dispatchBlocked ? disabled(
|
|
130
|
+
actions: { send: dispatchBlocked ? disabled("task-control-gate-closed") : available, hold: available, editHeld: available,
|
|
131
|
+
deleteHeld: available, dispatchHeld: dispatchBlocked ? disabled("task-control-gate-closed") : available,
|
|
132
|
+
interruptAndSend: dispatchBlocked ? disabled("task-control-gate-closed") : control.liveness === "running" ? available : disabled("agent-not-running") }
|
|
115
133
|
};
|
|
116
|
-
if (!terminal && this.#input.attachmentCapability) {
|
|
117
|
-
const attachment = this.#input.attachmentCapability();
|
|
118
|
-
value.snapshot.capabilities.capabilities.attachments = { status: "available", version: 1, reason: null,
|
|
119
|
-
kinds: attachment.kinds, mimeTypes: attachment.mimeTypes };
|
|
120
|
-
Object.assign(value.snapshot.capabilities.limits, { maxRequestBodyBytes: 11_250_000, maxAttachmentCount: 4,
|
|
121
|
-
maxAttachmentFileBytes: 8_388_608, maxAttachmentTotalBytes: 16_777_216 });
|
|
122
|
-
}
|
|
123
134
|
let modelCatalog: any;
|
|
124
135
|
try { modelCatalog = this.#input.modelCatalog?.(); }
|
|
125
136
|
catch { modelCatalog = null; }
|
|
@@ -190,6 +201,32 @@ export class CoreInspectionProvider implements WebUiReadModelProvider {
|
|
|
190
201
|
const source = (await this.#projection()).sourceViews;
|
|
191
202
|
return view === "working-tree" ? source.workingTree : source[view];
|
|
192
203
|
}
|
|
204
|
+
// The listing walks the project and every granted root, so it costs real
|
|
205
|
+
// filesystem work — hundreds of milliseconds on a large repository. It also
|
|
206
|
+
// changes only when files change, so a short cache keeps a browser that
|
|
207
|
+
// re-renders from re-walking the disk, without holding a stale view long
|
|
208
|
+
// enough for the operator to notice.
|
|
209
|
+
async documents(): Promise<unknown> {
|
|
210
|
+
const cached = this.#documentCache;
|
|
211
|
+
if (cached && Date.now() - cached.at <= 2_000) return cached.value;
|
|
212
|
+
const core = await loadCoreInspection();
|
|
213
|
+
const value = core.projectDocumentWorkspaceListing(this.#documentWorkspaceInput(core.matchesProtectedPath));
|
|
214
|
+
this.#documentCache = { at: Date.now(), value };
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async document(documentRef: string): Promise<unknown> {
|
|
219
|
+
const core = await loadCoreInspection();
|
|
220
|
+
return core.projectDocumentWorkspaceDocument({ ...this.#documentWorkspaceInput(core.matchesProtectedPath), documentRef });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#documentWorkspaceInput(matchesProtectedPath: (candidate: string, patterns: string[]) => unknown) {
|
|
224
|
+
const protectedPaths = this.#input.protectedPaths?.() ?? [];
|
|
225
|
+
return { cwd: this.#input.cwd, profileRoots: this.#input.documentReadRoots?.(),
|
|
226
|
+
environmentRoots: process.env.PIAGENT_ADDITIONAL_READ_ROOTS,
|
|
227
|
+
isProtectedPath: (candidate: string) => Boolean(matchesProtectedPath(candidate, protectedPaths)) };
|
|
228
|
+
}
|
|
229
|
+
|
|
193
230
|
async activity(): Promise<unknown> { return (await this.#projection()).snapshot.activity; }
|
|
194
231
|
async logPreview(activityRef: string): Promise<unknown> {
|
|
195
232
|
const activity = (await this.#projection()).snapshot.activity;
|
|
@@ -62,6 +62,8 @@ export class IpcReadModelClient implements WebUiReadModelProvider {
|
|
|
62
62
|
handoffHistory(runRef: string): Promise<unknown> { return this.#request("handoffHistory", [runRef]); }
|
|
63
63
|
subagentTree(runRef: string): Promise<unknown> { return this.#request("subagentTree", [runRef]); }
|
|
64
64
|
releaseMonitor(): Promise<unknown> { return this.#request("releaseMonitor"); }
|
|
65
|
+
documents(): Promise<unknown> { return this.#request("documents"); }
|
|
66
|
+
document(documentRef: string): Promise<unknown> { return this.#request("document", [documentRef]); }
|
|
65
67
|
activity(): Promise<unknown> { return this.#request("activity"); }
|
|
66
68
|
logPreview(activityRef: string): Promise<unknown> { return this.#request("logPreview", [activityRef]); }
|
|
67
69
|
transcript(beforeCursor: string | null, limit: number): Promise<unknown> { return this.#request("transcript", [beforeCursor, limit]); }
|
|
@@ -19,7 +19,8 @@ const MAX_JSON_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
|
19
19
|
const CURSOR = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
|
|
20
20
|
|
|
21
21
|
type ReadCapabilities = () => unknown | Promise<unknown>;
|
|
22
|
-
type RateState = { windowStart: number; requests: number; bootstraps: number
|
|
22
|
+
type RateState = { windowStart: number; requests: number; bootstraps: number };
|
|
23
|
+
type ControlRateState = { windowStart: number; controls: number };
|
|
23
24
|
|
|
24
25
|
export type LoopbackServer = {
|
|
25
26
|
origin: string;
|
|
@@ -57,6 +58,7 @@ export async function startLoopbackServer(options: {
|
|
|
57
58
|
readSessionCatalog?: () => unknown | Promise<unknown>;
|
|
58
59
|
readSessionCreationOptions?: () => unknown | Promise<unknown>;
|
|
59
60
|
readSessionModel?: (sessionRef: string) => WebUiReadModelProvider | Promise<WebUiReadModelProvider>;
|
|
61
|
+
executeSessionAttachment?: (sessionRef: string, command: unknown) => unknown | Promise<unknown>;
|
|
60
62
|
readSessionConnections?: (sessionRef: string) => unknown | Promise<unknown>;
|
|
61
63
|
executeSessionConnection?: (command: unknown) => unknown | Promise<unknown>;
|
|
62
64
|
readMcpAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
|
|
@@ -82,6 +84,15 @@ export async function startLoopbackServer(options: {
|
|
|
82
84
|
const auth = new SessionAuthority({ bootstrapTtlMs: options.bootstrapTtlMs, sessionTtlMs: options.sessionTtlMs });
|
|
83
85
|
const sse = options.readModel ? new SseHub(options.readModel) : null;
|
|
84
86
|
const rates = new Map<string, RateState>();
|
|
87
|
+
const controlRates = new Map<string, ControlRateState>();
|
|
88
|
+
const consumeControl = (sessionId: string, now: number): boolean => {
|
|
89
|
+
for (const [id, value] of controlRates) if (now - value.windowStart >= 60_000) controlRates.delete(id);
|
|
90
|
+
const value = controlRates.get(sessionId) ?? { windowStart: now, controls: 0 };
|
|
91
|
+
if (now - value.windowStart >= 60_000) Object.assign(value, { windowStart: now, controls: 0 });
|
|
92
|
+
value.controls += 1; controlRates.set(sessionId, value);
|
|
93
|
+
while (controlRates.size > 64) controlRates.delete(controlRates.keys().next().value as string);
|
|
94
|
+
return value.controls <= CONTROLS_PER_MINUTE;
|
|
95
|
+
};
|
|
85
96
|
let origin = "";
|
|
86
97
|
const server = http.createServer(async (request, response) => {
|
|
87
98
|
applySecurityHeaders(response, styleNonce);
|
|
@@ -90,9 +101,16 @@ export async function startLoopbackServer(options: {
|
|
|
90
101
|
const requestOrigin = request.headers.origin;
|
|
91
102
|
if (requestOrigin && requestOrigin !== origin) return errorResponse(response, 403, "origin-mismatch");
|
|
92
103
|
const remote = request.socket.remoteAddress ?? "unknown", now = Date.now();
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
// A browser session is already a launch-scoped authenticated capability.
|
|
105
|
+
// Charging every tab and every prior launch to one localhost IP lets a
|
|
106
|
+
// refresh or an unauthenticated local process lock out the active operator.
|
|
107
|
+
const browserSession = auth.authenticate(request, now);
|
|
108
|
+
const rateKey = browserSession ? `${remote}:${browserSession.id}` : `remote:${remote}`;
|
|
109
|
+
for (const [key, value] of rates) if (now - value.windowStart >= 60_000) rates.delete(key);
|
|
110
|
+
const rate = rates.get(rateKey) ?? { windowStart: now, requests: 0, bootstraps: 0 };
|
|
111
|
+
if (now - rate.windowStart >= 60_000) Object.assign(rate, { windowStart: now, requests: 0, bootstraps: 0 });
|
|
112
|
+
rate.requests += 1; rates.set(rateKey, rate);
|
|
113
|
+
while (rates.size > 128) rates.delete(rates.keys().next().value as string);
|
|
96
114
|
if (rate.requests > REQUESTS_PER_MINUTE) return errorResponse(response, 429, "rate-limit");
|
|
97
115
|
let url: URL;
|
|
98
116
|
try { url = new URL(request.url ?? "/", origin); }
|
|
@@ -122,10 +140,10 @@ export async function startLoopbackServer(options: {
|
|
|
122
140
|
|
|
123
141
|
if (request.method === "POST" && ["/api/v1/chat/messages", "/api/v1/session-options", "/api/v1/lifecycle",
|
|
124
142
|
"/api/v1/control/resume-and-continue", "/api/v1/reviews", "/api/v1/source-mutations", "/api/v1/source-handoffs"].includes(url.pathname) && options.executeControl) {
|
|
125
|
-
rate.controls += 1;
|
|
126
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
127
143
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
128
|
-
|
|
144
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
145
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
146
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
129
147
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
130
148
|
let command: unknown;
|
|
131
149
|
try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
|
|
@@ -135,10 +153,10 @@ export async function startLoopbackServer(options: {
|
|
|
135
153
|
}
|
|
136
154
|
|
|
137
155
|
if (request.method === "POST" && url.pathname === "/api/v1/projects/import" && options.executeProjectImport) {
|
|
138
|
-
rate.controls += 1;
|
|
139
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
140
156
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
141
|
-
|
|
157
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
158
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
159
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
142
160
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
143
161
|
let command: unknown;
|
|
144
162
|
try { command = JSON.parse((await requestBody(request, MAX_BOOTSTRAP_BODY_BYTES)).toString("utf8")); }
|
|
@@ -155,10 +173,10 @@ export async function startLoopbackServer(options: {
|
|
|
155
173
|
}
|
|
156
174
|
|
|
157
175
|
if (request.method === "POST" && url.pathname === "/api/v1/provider-auth" && options.executeProviderAuth) {
|
|
158
|
-
rate.controls += 1;
|
|
159
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
160
176
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
161
|
-
|
|
177
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
178
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
179
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
162
180
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
163
181
|
let command: unknown;
|
|
164
182
|
try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
|
|
@@ -172,10 +190,10 @@ export async function startLoopbackServer(options: {
|
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
if (request.method === "POST" && url.pathname === "/api/v1/session-connections" && options.executeSessionConnection) {
|
|
175
|
-
rate.controls += 1;
|
|
176
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
177
193
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
178
|
-
|
|
194
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
195
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
196
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
179
197
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
180
198
|
let command: unknown;
|
|
181
199
|
try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
|
|
@@ -188,9 +206,10 @@ export async function startLoopbackServer(options: {
|
|
|
188
206
|
}
|
|
189
207
|
|
|
190
208
|
if (request.method === "POST" && url.pathname.startsWith("/api/v1/mcp-auth/jobs/") && url.pathname.endsWith("/cancel") && options.cancelMcpAuthJob) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
209
|
+
if (requestOrigin !== origin) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
210
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
211
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
212
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
194
213
|
const jobRef = url.pathname.slice("/api/v1/mcp-auth/jobs/".length, -"/cancel".length);
|
|
195
214
|
if (!CURSOR.test(jobRef) || jobRef.includes("/") || url.search) return errorResponse(response, 400, "invalid-mcp-auth-job-ref");
|
|
196
215
|
try { return jsonResponse(response, 200, await options.cancelMcpAuthJob(jobRef)); }
|
|
@@ -198,10 +217,10 @@ export async function startLoopbackServer(options: {
|
|
|
198
217
|
}
|
|
199
218
|
|
|
200
219
|
if (request.method === "POST" && url.pathname === "/api/v1/attachments" && options.executeAttachment) {
|
|
201
|
-
rate.controls += 1;
|
|
202
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
203
220
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
204
|
-
|
|
221
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
222
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
223
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
205
224
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
206
225
|
let command: unknown;
|
|
207
226
|
try { command = JSON.parse((await requestBody(request, MAX_ATTACHMENT_BODY_BYTES)).toString("utf8")); }
|
|
@@ -210,11 +229,30 @@ export async function startLoopbackServer(options: {
|
|
|
210
229
|
catch { return errorResponse(response, 503, "attachment-runtime-unavailable"); }
|
|
211
230
|
}
|
|
212
231
|
|
|
232
|
+
// The Gateway drives many sessions, so an attachment has to name the one it
|
|
233
|
+
// belongs to in the path. The body is the same bounded stage/discard command
|
|
234
|
+
// the single-session route takes, and every guard in front of it is the same.
|
|
235
|
+
if (request.method === "POST" && url.pathname.startsWith("/api/v1/sessions/") && url.pathname.endsWith("/attachments")
|
|
236
|
+
&& options.executeSessionAttachment) {
|
|
237
|
+
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
238
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
239
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
240
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
241
|
+
const sessionRef = url.pathname.slice("/api/v1/sessions/".length, -"/attachments".length);
|
|
242
|
+
if (!CURSOR.test(sessionRef) || sessionRef.includes("/")) return errorResponse(response, 400, "invalid-session-attachment-ref");
|
|
243
|
+
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
244
|
+
let command: unknown;
|
|
245
|
+
try { command = JSON.parse((await requestBody(request, MAX_ATTACHMENT_BODY_BYTES)).toString("utf8")); }
|
|
246
|
+
catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-attachment-command"); }
|
|
247
|
+
try { return jsonResponse(response, 200, await options.executeSessionAttachment(sessionRef, command)); }
|
|
248
|
+
catch { return errorResponse(response, 503, "attachment-runtime-unavailable"); }
|
|
249
|
+
}
|
|
250
|
+
|
|
213
251
|
if (request.method === "POST" && url.pathname.startsWith("/api/v1/approvals/") && url.pathname.endsWith("/decision") && options.executeApproval) {
|
|
214
|
-
rate.controls += 1;
|
|
215
|
-
if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
|
|
216
252
|
if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
|
|
217
|
-
|
|
253
|
+
const mutationSession = auth.authorizeMutation(request);
|
|
254
|
+
if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
|
|
255
|
+
if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
|
|
218
256
|
if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
|
|
219
257
|
const approvalRef = url.pathname.slice("/api/v1/approvals/".length, -"/decision".length);
|
|
220
258
|
if (!CURSOR.test(approvalRef) || approvalRef.includes("/")) return errorResponse(response, 400, "invalid-approval-ref");
|
|
@@ -22,6 +22,8 @@ export type WebUiReadModelProvider = {
|
|
|
22
22
|
handoffHistory(runRef: string): unknown | Promise<unknown>;
|
|
23
23
|
subagentTree(runRef: string): unknown | Promise<unknown>;
|
|
24
24
|
releaseMonitor(): unknown | Promise<unknown>;
|
|
25
|
+
documents(): unknown | Promise<unknown>;
|
|
26
|
+
document(documentRef: string): unknown | Promise<unknown>;
|
|
25
27
|
activity(): unknown | Promise<unknown>;
|
|
26
28
|
logPreview(activityRef: string): unknown | Promise<unknown>;
|
|
27
29
|
transcript(beforeCursor: string | null, limit: number): unknown | Promise<unknown>;
|
|
@@ -29,6 +29,12 @@ export async function routeReadOnlyRequest(url: URL, provider: WebUiReadModelPro
|
|
|
29
29
|
if (url.search || !OPAQUE_REF.test(ref) || ref.includes("/")) return { handled: true, status: 400, value: { error: { code: "invalid-task-run-ref" } } };
|
|
30
30
|
return { handled: true, status: 200, value: await provider.subagentTree(ref) };
|
|
31
31
|
}
|
|
32
|
+
if (url.pathname === "/api/v1/documents" && !url.search) return { handled: true, status: 200, value: await provider.documents() };
|
|
33
|
+
if (url.pathname.startsWith("/api/v1/documents/")) {
|
|
34
|
+
const ref = url.pathname.slice("/api/v1/documents/".length);
|
|
35
|
+
if (url.search || !OPAQUE_REF.test(ref) || ref.includes("/")) return { handled: true, status: 400, value: { error: { code: "invalid-document-ref" } } };
|
|
36
|
+
return { handled: true, status: 200, value: await provider.document(ref) };
|
|
37
|
+
}
|
|
32
38
|
if (url.pathname === "/api/v1/chat/queue" && !url.search) return { handled: true, status: 200, value: await provider.queue() };
|
|
33
39
|
if (url.pathname === "/api/v1/session-options/models" && !url.search) return { handled: true, status: 200, value: await provider.modelCatalog() };
|
|
34
40
|
if (url.pathname.startsWith("/api/v1/approvals/")) {
|
|
@@ -13,8 +13,10 @@ type TranscriptRevision = { runtimeRevision: string; taskRevision: string | null
|
|
|
13
13
|
indexRevision: string | null; approvalRevision: string | null; sessionOptionRevision: string | null; queueRevision: string | null };
|
|
14
14
|
type TranscriptContent = { state: "available" | "redacted" | "unavailable"; text: string | null; textChars: number | null; digest: string | null;
|
|
15
15
|
truncated: boolean; redacted: boolean; imageCount: number; reasonCode: string | null };
|
|
16
|
+
type TranscriptAttachment = { displayName: string; kind: "file" | "image" | "document"; mimeType: string; truncated: boolean };
|
|
16
17
|
type TranscriptItem = { messageRef: string; parentMessageRef: string | null; role: "user" | "assistant" | "tool-result"; recordedAt: string;
|
|
17
18
|
agentOperationId: string | null; turnIndex: number | null; content: TranscriptContent;
|
|
19
|
+
attachments?: TranscriptAttachment[];
|
|
18
20
|
toolCalls: Array<{ toolCallRef: string; toolName: string; state: "requested" | "completed" | "failed" | "unknown" }> };
|
|
19
21
|
export type TranscriptDocument = { schemaVersion: 1; version: "piagent-webui-transcript-v1"; generatedAt: string; identity: TranscriptIdentity;
|
|
20
22
|
revision: TranscriptRevision; eventCursor: string; state: "ready" | "unavailable"; items: TranscriptItem[];
|
|
@@ -41,6 +43,50 @@ function messageText(message: any): string {
|
|
|
41
43
|
if (!Array.isArray(message?.content)) return "";
|
|
42
44
|
return message.content.filter((item: any) => item?.type === "text" && typeof item.text === "string").map((item: any) => item.text).join("\n");
|
|
43
45
|
}
|
|
46
|
+
function safeAttachmentName(value: unknown): string {
|
|
47
|
+
const projected = safeText(String(value ?? "attachment"));
|
|
48
|
+
return projected.full.replace(/[\\/]/g, "_").replace(/\s+/g, " ").trim().slice(0, 160) || "attachment";
|
|
49
|
+
}
|
|
50
|
+
function userMessageProjection(message: any): { text: string; attachments: TranscriptAttachment[] } {
|
|
51
|
+
if (message?.role !== "user" || !Array.isArray(message?.content)) return { text: messageText(message), attachments: [] };
|
|
52
|
+
const textParts = message.content.filter((part: any) => part?.type === "text" && typeof part.text === "string");
|
|
53
|
+
const attachments: TranscriptAttachment[] = [];
|
|
54
|
+
for (const part of textParts.slice(1)) {
|
|
55
|
+
const lines = String(part.text).split("\n", 3);
|
|
56
|
+
if (!lines[0]?.startsWith("attached file: ") || !lines[1]?.startsWith("format: ")) continue;
|
|
57
|
+
let displayName: unknown;
|
|
58
|
+
try { displayName = JSON.parse(lines[0].slice("attached file: ".length)); } catch { continue; }
|
|
59
|
+
if (typeof displayName !== "string") continue;
|
|
60
|
+
const format = /^format: ([A-Za-z0-9][A-Za-z0-9.+-]*\/[A-Za-z0-9][A-Za-z0-9.+-]*)(, truncated)?$/.exec(lines[1]);
|
|
61
|
+
if (!format) continue;
|
|
62
|
+
const mimeType = format[1]!;
|
|
63
|
+
const kind = mimeType === "application/pdf" || mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
64
|
+
? "document" as const : "file" as const;
|
|
65
|
+
attachments.push({ displayName: safeAttachmentName(displayName), kind, mimeType, truncated: Boolean(format[2]) });
|
|
66
|
+
}
|
|
67
|
+
for (const part of message.content) {
|
|
68
|
+
if (part?.type !== "image" || typeof part.mimeType !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.+-]*\/[A-Za-z0-9][A-Za-z0-9.+-]*$/.test(part.mimeType)) continue;
|
|
69
|
+
attachments.push({ displayName: "Image", kind: "image", mimeType: part.mimeType.slice(0, 120), truncated: false });
|
|
70
|
+
}
|
|
71
|
+
// Attachment bodies are provider input, not transcript prose. Showing or
|
|
72
|
+
// hashing them here would turn the chat bubble into a document dump and make
|
|
73
|
+
// a bounded UI digest an oracle over user files. The first block is the exact
|
|
74
|
+
// text the operator typed; the remaining recognized blocks become cards.
|
|
75
|
+
return { text: attachments.length ? String(textParts[0]?.text ?? "") : messageText(message), attachments: attachments.slice(0, 4) };
|
|
76
|
+
}
|
|
77
|
+
function assistantFailureReason(message: any): string | null {
|
|
78
|
+
if (message?.role !== "assistant" || message?.stopReason !== "error") return null;
|
|
79
|
+
const detail = String(message?.errorMessage ?? "").toLowerCase();
|
|
80
|
+
if (/(?:authentication|auth|token|credential).{0,48}expired|expired.{0,48}(?:authentication|auth|token|credential)/.test(detail)) {
|
|
81
|
+
return "provider-auth-expired";
|
|
82
|
+
}
|
|
83
|
+
if (/(?:authentication|unauthorized|forbidden|credential|api[ _-]?key|log[ -]?in|sign[ -]?in)/.test(detail)) {
|
|
84
|
+
return "provider-auth-required";
|
|
85
|
+
}
|
|
86
|
+
if (/(?:rate[ _-]?limit|too many requests|quota)/.test(detail)) return "provider-rate-limited";
|
|
87
|
+
if (/(?:network|fetch failed|econn|timed? ?out|unavailable)/.test(detail)) return "provider-unavailable";
|
|
88
|
+
return "provider-response-failed";
|
|
89
|
+
}
|
|
44
90
|
function safeText(value: string): { full: string; preview: string; redacted: boolean; truncated: boolean } {
|
|
45
91
|
const clean = value.replace(ANSI, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
|
|
46
92
|
const redaction = redactSensitiveText(clean);
|
|
@@ -69,21 +115,31 @@ function toolCalls(message: any, sessionRef: string, role: TranscriptItem["role"
|
|
|
69
115
|
function role(message: any): TranscriptItem["role"] | null {
|
|
70
116
|
return message?.role === "user" ? "user" : message?.role === "assistant" ? "assistant" : message?.role === "toolResult" ? "tool-result" : null;
|
|
71
117
|
}
|
|
118
|
+
function isInternalFreshTransition(message: any): boolean {
|
|
119
|
+
if (message?.role !== "user") return false;
|
|
120
|
+
return /^\/fresh\s+(?:task|scout|be-to-fe)\s+(?:--session-title\s+"[^"\r\n]{1,64}"\s+)?Read task intake from \.pi\/task-inbox\/[A-Za-z0-9._-]+\.md\.\s+Current session is near context limits; use a fresh governed session\.$/u
|
|
121
|
+
.test(messageText(message).trim());
|
|
122
|
+
}
|
|
72
123
|
function unavailableContent(reasonCode: string): TranscriptContent {
|
|
73
124
|
return { state: "unavailable", text: null, textChars: null, digest: null, truncated: false, redacted: false, imageCount: 0, reasonCode };
|
|
74
125
|
}
|
|
75
126
|
function item(entry: any, identity: TranscriptIdentity): TranscriptItem | null {
|
|
76
127
|
if (!entry || entry.type !== "message" || !entry.message) return null;
|
|
128
|
+
if (isInternalFreshTransition(entry.message)) return null;
|
|
77
129
|
const itemRole = role(entry.message), recordedAt = timestamp(entry.timestamp ?? entry.message.timestamp);
|
|
78
130
|
if (!itemRole || !recordedAt || typeof entry.id !== "string" || entry.id.length === 0) return null;
|
|
131
|
+
const userProjection = userMessageProjection(entry.message);
|
|
79
132
|
const content = itemRole === "tool-result" ? unavailableContent("tool-output-in-activity-preview") : (() => {
|
|
80
|
-
const projected = safeText(messageText(entry.message));
|
|
133
|
+
const projected = safeText(itemRole === "user" ? userProjection.text : messageText(entry.message));
|
|
134
|
+
const failureReason = projected.full.length === 0 ? assistantFailureReason(entry.message) : null;
|
|
135
|
+
if (failureReason) return unavailableContent(failureReason);
|
|
81
136
|
return { state: projected.redacted ? "redacted" as const : "available" as const, text: projected.preview,
|
|
82
137
|
textChars: Math.min(1_000_000_000, projected.full.length), digest: `sha256:${hash(projected.full)}`, truncated: projected.truncated,
|
|
83
138
|
redacted: projected.redacted, imageCount: imageCount(entry.message), reasonCode: projected.redacted ? "sensitive-values-redacted" as const : null };
|
|
84
139
|
})();
|
|
85
140
|
return { messageRef: opaque("message", [identity.sessionRef, entry.id]), parentMessageRef: null, role: itemRole, recordedAt,
|
|
86
|
-
agentOperationId: null, turnIndex: null, content,
|
|
141
|
+
agentOperationId: null, turnIndex: null, content, ...(userProjection.attachments.length ? { attachments: userProjection.attachments } : {}),
|
|
142
|
+
toolCalls: toolCalls(entry.message, identity.sessionRef, itemRole) };
|
|
87
143
|
}
|
|
88
144
|
function unavailable(input: TranscriptProjectionInput, reasonCode: string, limit: number): TranscriptDocument {
|
|
89
145
|
return { schemaVersion: 1, version: "piagent-webui-transcript-v1", generatedAt: input.generatedAt ?? new Date().toISOString(),
|
|
@@ -49,7 +49,12 @@
|
|
|
49
49
|
"image/bmp",
|
|
50
50
|
"text/plain",
|
|
51
51
|
"text/markdown",
|
|
52
|
-
"
|
|
52
|
+
"text/csv",
|
|
53
|
+
"text/tab-separated-values",
|
|
54
|
+
"application/json",
|
|
55
|
+
"application/yaml",
|
|
56
|
+
"application/pdf",
|
|
57
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
53
58
|
]
|
|
54
59
|
},
|
|
55
60
|
"dataBase64": {
|
|
@@ -86,7 +91,7 @@
|
|
|
86
91
|
"attachment": {
|
|
87
92
|
"type": "object",
|
|
88
93
|
"additionalProperties": false,
|
|
89
|
-
"required": ["attachmentRef", "messageRequestId", "displayName", "kind", "mimeType", "sizeBytes", "expiresAt"],
|
|
94
|
+
"required": ["attachmentRef", "messageRequestId", "displayName", "kind", "mimeType", "sizeBytes", "sourceBytes", "truncated", "expiresAt"],
|
|
90
95
|
"properties": {
|
|
91
96
|
"attachmentRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
92
97
|
"messageRequestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
|
|
@@ -96,14 +101,29 @@
|
|
|
96
101
|
"maxLength": 160,
|
|
97
102
|
"pattern": "^[^\\u0000-\\u001F\\u007F/\\\\]*$"
|
|
98
103
|
},
|
|
99
|
-
"kind": { "enum": ["file", "image"] },
|
|
104
|
+
"kind": { "enum": ["file", "image", "document"] },
|
|
100
105
|
"mimeType": {
|
|
101
106
|
"type": "string",
|
|
102
107
|
"minLength": 3,
|
|
103
108
|
"maxLength": 127,
|
|
104
109
|
"pattern": "^[a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+*-]+$"
|
|
105
110
|
},
|
|
106
|
-
"sizeBytes": {
|
|
111
|
+
"sizeBytes": {
|
|
112
|
+
"description": "Bytes held for dispatch: image bytes, or the UTF-8 text extracted from a text or document file.",
|
|
113
|
+
"type": "integer",
|
|
114
|
+
"minimum": 1,
|
|
115
|
+
"maximum": 8388608
|
|
116
|
+
},
|
|
117
|
+
"sourceBytes": {
|
|
118
|
+
"description": "Bytes of the file the operator selected, before extraction. Equal to sizeBytes for images.",
|
|
119
|
+
"type": "integer",
|
|
120
|
+
"minimum": 1,
|
|
121
|
+
"maximum": 8388608
|
|
122
|
+
},
|
|
123
|
+
"truncated": {
|
|
124
|
+
"description": "True when extraction produced more text than the per-attachment cap, so Pi receives a prefix.",
|
|
125
|
+
"type": "boolean"
|
|
126
|
+
},
|
|
107
127
|
"expiresAt": { "$ref": "common-v1.schema.json#/$defs/timestamp" }
|
|
108
128
|
}
|
|
109
129
|
},
|
|
@@ -535,9 +535,9 @@
|
|
|
535
535
|
"kinds": {
|
|
536
536
|
"type": "array",
|
|
537
537
|
"minItems": 1,
|
|
538
|
-
"maxItems":
|
|
538
|
+
"maxItems": 3,
|
|
539
539
|
"uniqueItems": true,
|
|
540
|
-
"items": { "enum": ["file", "image"] }
|
|
540
|
+
"items": { "enum": ["file", "image", "document"] }
|
|
541
541
|
},
|
|
542
542
|
"mimeTypes": {
|
|
543
543
|
"type": "array",
|
|
@@ -173,6 +173,15 @@
|
|
|
173
173
|
"supported": true,
|
|
174
174
|
"current": true
|
|
175
175
|
},
|
|
176
|
+
{
|
|
177
|
+
"name": "document-workspace-v1",
|
|
178
|
+
"file": "document-workspace-v1.schema.json",
|
|
179
|
+
"id": "https://piagent.io.vn/schemas/piagent-webui/document-workspace-v1.schema.json",
|
|
180
|
+
"documentVersion": 1,
|
|
181
|
+
"role": "read-model",
|
|
182
|
+
"supported": true,
|
|
183
|
+
"current": true
|
|
184
|
+
},
|
|
176
185
|
{
|
|
177
186
|
"name": "control-command-v1",
|
|
178
187
|
"file": "control-command-v1.schema.json",
|