@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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Nothing here is a secret. Every credential is referenced as an environment
|
|
7
7
|
// variable, so the config can be read, diffed and committed without leaking one.
|
|
8
8
|
|
|
9
|
-
/** @typedef {{description: string, config: Record<string, unknown>}} CatalogServer */
|
|
9
|
+
/** @typedef {{description: string, maturity?: "stable"|"experimental", config: Record<string, unknown>}} CatalogServer */
|
|
10
10
|
|
|
11
11
|
/** @type {Record<string, CatalogServer>} */
|
|
12
12
|
export const CATALOG_SERVERS = {
|
|
@@ -83,6 +83,85 @@ export const CATALOG_SERVERS = {
|
|
|
83
83
|
lifecycle: "lazy",
|
|
84
84
|
directTools: false
|
|
85
85
|
}
|
|
86
|
+
},
|
|
87
|
+
// Google's own remote servers, one per product, rather than a community server
|
|
88
|
+
// holding every scope at once: a session that only needs to read a document
|
|
89
|
+
// should not be one OAuth grant away from the mailbox.
|
|
90
|
+
//
|
|
91
|
+
// These carry no version pin because there is nothing local to pin -- the
|
|
92
|
+
// endpoint is operated by Google and changes under the same URL. That is a
|
|
93
|
+
// real difference from every command-based server above, where the pin is what
|
|
94
|
+
// stops a floating tag becoming a supply-chain decision each session.
|
|
95
|
+
// Scopes are declared read-only. Write access is one word longer to ask for
|
|
96
|
+
// and cannot be taken back once an agent has used it, so the default grant is
|
|
97
|
+
// the one that cannot lose anything. `clientId` is deliberately absent: every
|
|
98
|
+
// operator registers their own OAuth client, and a client id baked in here
|
|
99
|
+
// would be someone else's project answering for their consent screen.
|
|
100
|
+
"google-drive": {
|
|
101
|
+
description: "EXPERIMENTAL Developer Preview: Google Drive remote MCP server. Read-only by default.",
|
|
102
|
+
maturity: "experimental",
|
|
103
|
+
config: {
|
|
104
|
+
url: "https://drivemcp.googleapis.com/mcp/v1",
|
|
105
|
+
auth: "oauth",
|
|
106
|
+
oauth: { scopes: ["https://www.googleapis.com/auth/drive.readonly"] },
|
|
107
|
+
lifecycle: "lazy",
|
|
108
|
+
directTools: false
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
gmail: {
|
|
112
|
+
description: "EXPERIMENTAL Developer Preview: Gmail remote MCP server. Read-only by default.",
|
|
113
|
+
maturity: "experimental",
|
|
114
|
+
config: {
|
|
115
|
+
url: "https://gmailmcp.googleapis.com/mcp/v1",
|
|
116
|
+
auth: "oauth",
|
|
117
|
+
oauth: { scopes: ["https://www.googleapis.com/auth/gmail.readonly"] },
|
|
118
|
+
lifecycle: "lazy",
|
|
119
|
+
directTools: false
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"google-docs": {
|
|
123
|
+
description: "EXPERIMENTAL Developer Preview: Google Docs remote MCP server. Read-only by default.",
|
|
124
|
+
maturity: "experimental",
|
|
125
|
+
config: {
|
|
126
|
+
url: "https://docsmcp.googleapis.com/mcp/v1",
|
|
127
|
+
auth: "oauth",
|
|
128
|
+
oauth: {
|
|
129
|
+
scopes: [
|
|
130
|
+
"https://www.googleapis.com/auth/documents.readonly",
|
|
131
|
+
"https://www.googleapis.com/auth/drive.readonly"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
lifecycle: "lazy",
|
|
135
|
+
directTools: false
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"google-sheets": {
|
|
139
|
+
description: "EXPERIMENTAL Developer Preview: Google Sheets remote MCP server. Read-only by default.",
|
|
140
|
+
maturity: "experimental",
|
|
141
|
+
config: {
|
|
142
|
+
url: "https://sheetsmcp.googleapis.com/mcp/v1",
|
|
143
|
+
auth: "oauth",
|
|
144
|
+
oauth: {
|
|
145
|
+
scopes: [
|
|
146
|
+
"https://www.googleapis.com/auth/spreadsheets.readonly",
|
|
147
|
+
"https://www.googleapis.com/auth/drive.readonly"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
lifecycle: "lazy",
|
|
151
|
+
directTools: false
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
// Converts .docx, .xlsx, .pptx and .pdf to Markdown locally. Local matters:
|
|
155
|
+
// the alternative to this is uploading a document somewhere to read it, and a
|
|
156
|
+
// document worth converting is usually one worth not uploading.
|
|
157
|
+
markitdown: {
|
|
158
|
+
description: "MarkItDown MCP. Converts local docx, xlsx, pptx, pdf and html files to Markdown. Runs offline for file: URIs.",
|
|
159
|
+
config: {
|
|
160
|
+
command: "uvx",
|
|
161
|
+
args: ["markitdown-mcp@0.0.1a4"],
|
|
162
|
+
lifecycle: "lazy",
|
|
163
|
+
directTools: false
|
|
164
|
+
}
|
|
86
165
|
}
|
|
87
166
|
};
|
|
88
167
|
|
|
@@ -100,7 +179,33 @@ export const CATALOG_PREREQUISITES = {
|
|
|
100
179
|
],
|
|
101
180
|
playwright: ["browsers installed on first use (npx playwright install)"],
|
|
102
181
|
figma: ["Piagent approved in the Figma MCP Catalog before remote OAuth can register"],
|
|
103
|
-
"figma-desktop": ["Figma desktop running with Dev Mode MCP enabled"]
|
|
182
|
+
"figma-desktop": ["Figma desktop running with Dev Mode MCP enabled"],
|
|
183
|
+
"google-drive": [
|
|
184
|
+
"a Google Cloud project with drive.googleapis.com and drivemcp.googleapis.com enabled",
|
|
185
|
+
"Developer Preview: no Pi OAuth end-to-end release proof exists yet",
|
|
186
|
+
"Google's supported client guides use an OAuth client id and secret; Pi's public-client/PKCE compatibility is unverified",
|
|
187
|
+
"piagent-mcp add google-drive --url https://drivemcp.googleapis.com/mcp/v1 --oauth-client-id <id> --scope user"
|
|
188
|
+
],
|
|
189
|
+
gmail: [
|
|
190
|
+
"a Google Cloud project with gmail.googleapis.com and gmailmcp.googleapis.com enabled",
|
|
191
|
+
"Developer Preview: no Pi OAuth end-to-end release proof exists yet",
|
|
192
|
+
"Google's supported client guides use an OAuth client id and secret; Pi's public-client/PKCE compatibility is unverified",
|
|
193
|
+
"read the warning in docs/mcp-and-tools.md first: mail is untrusted input reaching an agent that can act"
|
|
194
|
+
],
|
|
195
|
+
"google-docs": [
|
|
196
|
+
"a Google Cloud project with docs.googleapis.com and docsmcp.googleapis.com enabled",
|
|
197
|
+
"Developer Preview: no Pi OAuth end-to-end release proof exists yet",
|
|
198
|
+
"Google's supported client guides use an OAuth client id and secret; Pi's public-client/PKCE compatibility is unverified"
|
|
199
|
+
],
|
|
200
|
+
"google-sheets": [
|
|
201
|
+
"a Google Cloud project with sheets.googleapis.com and sheetsmcp.googleapis.com enabled",
|
|
202
|
+
"Developer Preview: no Pi OAuth end-to-end release proof exists yet",
|
|
203
|
+
"Google's supported client guides use an OAuth client id and secret; Pi's public-client/PKCE compatibility is unverified"
|
|
204
|
+
],
|
|
205
|
+
markitdown: [
|
|
206
|
+
"uv on PATH (https://docs.astral.sh/uv); the first run downloads a Python runtime and roughly 120 MB of dependencies",
|
|
207
|
+
"note: markitdown-mcp is published as an alpha, so pin changes deliberately"
|
|
208
|
+
]
|
|
104
209
|
};
|
|
105
210
|
|
|
106
211
|
// Variables a pinned server references but works without. Context7 answers
|
|
@@ -113,6 +218,11 @@ export const CATALOG_OPTIONAL_ENV = {
|
|
|
113
218
|
context7: ["CONTEXT7_API_KEY"]
|
|
114
219
|
};
|
|
115
220
|
|
|
221
|
+
// `core` is what an install writes when nobody names a preset, and `popular` is
|
|
222
|
+
// what somebody picks without reading the list. Neither may reach a mailbox or a
|
|
223
|
+
// Drive: an install that quietly grants those is a decision the operator never
|
|
224
|
+
// made. The Google servers are therefore only in presets somebody has to type,
|
|
225
|
+
// and in `all`, which means what it says.
|
|
116
226
|
/** @type {Record<string, string[]>} */
|
|
117
227
|
export const CATALOG_PRESETS = {
|
|
118
228
|
minimal: [],
|
|
@@ -122,9 +232,24 @@ export const CATALOG_PRESETS = {
|
|
|
122
232
|
design: ["figma-desktop"],
|
|
123
233
|
"design-local": ["figma-desktop"],
|
|
124
234
|
web: ["context7", "chrome-devtools", "playwright"],
|
|
235
|
+
// The generic documents preset stays local. Google Workspace is Developer
|
|
236
|
+
// Preview and must be selected through an explicitly named Google preset.
|
|
237
|
+
documents: ["markitdown"],
|
|
238
|
+
// One preset per product, because that is why Google ships four servers
|
|
239
|
+
// instead of one. Bundling them back together would mean reading a
|
|
240
|
+
// spreadsheet costs a grant over Drive and every document in it.
|
|
241
|
+
"google-drive": ["google-drive"],
|
|
242
|
+
"google-docs": ["google-docs"],
|
|
243
|
+
"google-sheets": ["google-sheets"],
|
|
244
|
+
"google-mail": ["gmail"],
|
|
245
|
+
google: ["google-drive", "google-docs", "google-sheets"],
|
|
246
|
+
"google-all": ["google-drive", "gmail", "google-docs", "google-sheets"],
|
|
125
247
|
core: ["context7", "chrome-devtools", "github"],
|
|
126
248
|
popular: ["context7", "chrome-devtools", "playwright", "github", "figma-desktop"],
|
|
127
|
-
all: [
|
|
249
|
+
all: [
|
|
250
|
+
"context7", "chrome-devtools", "playwright", "github", "figma-desktop", "figma",
|
|
251
|
+
"markitdown", "google-drive", "gmail", "google-docs", "google-sheets"
|
|
252
|
+
]
|
|
128
253
|
};
|
|
129
254
|
|
|
130
255
|
/** @param {string} name @returns {boolean} */
|
|
@@ -136,7 +261,7 @@ export function isCatalogPreset(name) {
|
|
|
136
261
|
* The servers a preset installs, with their definitions resolved. Throws on an
|
|
137
262
|
* unknown preset so a typo is refused before anything is written.
|
|
138
263
|
* @param {string} name
|
|
139
|
-
* @returns {{name: string, config: Record<string, unknown>, requires: string[]}[]}
|
|
264
|
+
* @returns {{name: string, maturity: "stable"|"experimental", config: Record<string, unknown>, requires: string[]}[]}
|
|
140
265
|
*/
|
|
141
266
|
export function resolvePreset(name) {
|
|
142
267
|
if (!isCatalogPreset(name)) {
|
|
@@ -145,6 +270,11 @@ export function resolvePreset(name) {
|
|
|
145
270
|
return CATALOG_PRESETS[name].map((serverName) => {
|
|
146
271
|
const server = CATALOG_SERVERS[serverName];
|
|
147
272
|
if (!server) throw new Error(`preset ${name} references missing server ${serverName}`);
|
|
148
|
-
return {
|
|
273
|
+
return {
|
|
274
|
+
name: serverName,
|
|
275
|
+
maturity: server.maturity ?? "stable",
|
|
276
|
+
config: server.config,
|
|
277
|
+
requires: CATALOG_PREREQUISITES[serverName] ?? []
|
|
278
|
+
};
|
|
149
279
|
});
|
|
150
280
|
}
|
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
4
|
import { toolResultFingerprint } from "../../extensions/context-engine.js";
|
|
5
|
+
import { withRemedy } from "../policy/block-remedy.ts";
|
|
5
6
|
import { observeTrajectorySync } from "../trajectory/trajectory-observability.ts";
|
|
6
7
|
import type { TrajectorySyncResult } from "../trajectory/trajectory-runtime.ts";
|
|
7
8
|
|
|
@@ -75,7 +76,13 @@ export function registerToolCallHook(pi: ExtensionAPI, dependencies: ToolCallHoo
|
|
|
75
76
|
: dependencies.reviewBudgetDecision?.(resolvedEvent, ctx);
|
|
76
77
|
const startDecision = !(decision && typeof decision === "object" && decision.block) ? dependencies.beforeStart?.(resolvedEvent, ctx) : undefined;
|
|
77
78
|
const finalDecision = startDecision && typeof startDecision === "object" && startDecision.block ? startDecision : decision;
|
|
78
|
-
|
|
79
|
+
// Every refusal leaves through here, so this is where it is answered. A
|
|
80
|
+
// remedy attached at the 29 sites that build one is a remedy the next site
|
|
81
|
+
// will forget.
|
|
82
|
+
const answered = finalDecision && typeof finalDecision === "object" && finalDecision.block
|
|
83
|
+
? withRemedy(finalDecision)
|
|
84
|
+
: finalDecision;
|
|
85
|
+
const blockedDecision = answered && typeof answered === "object" ? answered : undefined;
|
|
79
86
|
record(ctx, {
|
|
80
87
|
activityId: `decision:${toolCallId}`,
|
|
81
88
|
event: "tool_decision",
|
|
@@ -91,6 +98,6 @@ export function registerToolCallHook(pi: ExtensionAPI, dependencies: ToolCallHoo
|
|
|
91
98
|
observeTrajectorySync(ctx, dependencies.afterAuthorized?.(resolvedEvent, ctx, { toolCallId }), dependencies.telemetry);
|
|
92
99
|
}
|
|
93
100
|
dependencies.afterDecision?.(resolvedEvent, ctx, { toolCallId, allowed: !blockedDecision?.block, reason: blockedDecision?.reason });
|
|
94
|
-
return
|
|
101
|
+
return answered;
|
|
95
102
|
});
|
|
96
103
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { MAX_DOCUMENT_BYTES, PDF_TIMEOUT_MS, probeExecutableOnPath,
|
|
4
|
+
type DocumentCommandResult } from "../../extensions/document-intake.ts";
|
|
5
|
+
|
|
6
|
+
export type AttachmentDocumentCommand = (executable: string, args: string[], input?: Buffer) =>
|
|
7
|
+
DocumentCommandResult | Promise<DocumentCommandResult>;
|
|
8
|
+
|
|
9
|
+
// The converter is external and may consume its full timeout. Run it without a
|
|
10
|
+
// shell and without blocking Pi's event loop, while bounding both output pipes.
|
|
11
|
+
export function runAttachmentDocumentCommand(executable: string, args: string[], input?: Buffer):
|
|
12
|
+
DocumentCommandResult | Promise<DocumentCommandResult> {
|
|
13
|
+
if (executable === "command") return probeExecutableOnPath(args[1] ?? "");
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
let child: ReturnType<typeof spawn>;
|
|
16
|
+
try { child = spawn(executable, args, { stdio: ["pipe", "pipe", "pipe"] }); }
|
|
17
|
+
catch (cause) { resolve({ status: null, stdout: "", stderr: "", error: cause as Error }); return; }
|
|
18
|
+
const stdout: Buffer[] = [], stderr: Buffer[] = []; let stdoutBytes = 0, stderrBytes = 0, settled = false;
|
|
19
|
+
const finish = (result: DocumentCommandResult) => {
|
|
20
|
+
if (settled) return; settled = true; clearTimeout(timer); resolve(result);
|
|
21
|
+
};
|
|
22
|
+
const overflow = () => {
|
|
23
|
+
child.kill("SIGKILL");
|
|
24
|
+
const error = new Error("document converter output exceeded its limit"); (error as NodeJS.ErrnoException).code = "ENOBUFS";
|
|
25
|
+
finish({ status: null, stdout: "", stderr: "", error });
|
|
26
|
+
};
|
|
27
|
+
child.stdout.on("data", (chunk: Buffer) => { stdoutBytes += chunk.length; if (stdoutBytes > MAX_DOCUMENT_BYTES) overflow(); else stdout.push(chunk); });
|
|
28
|
+
child.stderr.on("data", (chunk: Buffer) => { stderrBytes += chunk.length; if (stderrBytes > MAX_DOCUMENT_BYTES) overflow(); else stderr.push(chunk); });
|
|
29
|
+
child.on("error", (error) => finish({ status: null, stdout: "", stderr: "", error }));
|
|
30
|
+
child.on("close", (status) => finish({ status, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") }));
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
child.kill("SIGKILL"); const error = new Error("document converter ETIMEDOUT");
|
|
33
|
+
finish({ status: null, stdout: "", stderr: "", error });
|
|
34
|
+
}, PDF_TIMEOUT_MS);
|
|
35
|
+
timer.unref();
|
|
36
|
+
child.stdin.on("error", () => undefined);
|
|
37
|
+
child.stdin.end(input);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type AttachmentReservation<T> = {
|
|
2
|
+
prepared: T;
|
|
3
|
+
commit(): void;
|
|
4
|
+
release(): void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// A small transaction primitive for staged attachments. The map is the lock:
|
|
8
|
+
// only one dispatch may prepare a ref, and only that dispatch may consume it.
|
|
9
|
+
export function reserveAttachments<TRecord extends { attachmentRef: string }, TPrepared>(input: {
|
|
10
|
+
records: TRecord[];
|
|
11
|
+
live: Map<string, TRecord>;
|
|
12
|
+
reservations: Map<string, string>;
|
|
13
|
+
token: string;
|
|
14
|
+
prepared: TPrepared;
|
|
15
|
+
consume(record: TRecord): void;
|
|
16
|
+
}): AttachmentReservation<TPrepared> {
|
|
17
|
+
for (const record of input.records) input.reservations.set(record.attachmentRef, input.token);
|
|
18
|
+
let state: "active" | "committed" | "released" = "active";
|
|
19
|
+
return {
|
|
20
|
+
prepared: input.prepared,
|
|
21
|
+
commit: () => {
|
|
22
|
+
if (state === "committed") return;
|
|
23
|
+
if (state !== "active" || input.records.some((record) => input.live.get(record.attachmentRef) !== record
|
|
24
|
+
|| input.reservations.get(record.attachmentRef) !== input.token)) throw new Error("attachment-reservation-unavailable");
|
|
25
|
+
state = "committed";
|
|
26
|
+
input.records.forEach(input.consume);
|
|
27
|
+
},
|
|
28
|
+
release: () => {
|
|
29
|
+
if (state !== "active") return;
|
|
30
|
+
state = "released";
|
|
31
|
+
for (const record of input.records) {
|
|
32
|
+
if (input.reservations.get(record.attachmentRef) === input.token) input.reservations.delete(record.attachmentRef);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|