@llblab/pi-actors 0.56.1 → 0.57.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/AGENTS.md +7 -7
- package/BACKLOG.md +4 -14
- package/CHANGELOG.md +13 -0
- package/README.md +7 -2
- package/dist/lib/execution-output.js +13 -2
- package/dist/lib/observability.js +9 -14
- package/dist/lib/registry.js +82 -117
- package/dist/lib/run-delivery.d.ts +1 -1
- package/dist/lib/run-delivery.js +146 -32
- package/dist/lib/run-ui-runtime.js +18 -27
- package/dist/lib/runs-process-windows-mi.d.ts +71 -0
- package/dist/lib/runs-process-windows-mi.js +146 -3
- package/dist/lib/tools-inspect.js +109 -59
- package/dist/lib/tools-local.js +16 -8
- package/dist/lib/tools-message.js +76 -65
- package/dist/lib/tools-response.d.ts +18 -0
- package/dist/lib/tools-response.js +88 -21
- package/dist/lib/tools-spawn.js +19 -12
- package/dist/skills/actors/SKILL.md +1 -1
- package/dist/skills/actors/references/diagnostics.md +3 -0
- package/dist/skills/actors/references/runs.md +1 -1
- package/docs/README.md +1 -0
- package/docs/actor-inspector.md +2 -2
- package/docs/agent-experience.md +51 -4
- package/docs/coordinator-delivery.md +2 -2
- package/docs/inactive-foundations-inventory.md +73 -0
- package/docs/inspection.md +9 -8
- package/docs/output-projection-map.md +19 -17
- package/docs/windows-process-observation.md +2 -2
- package/lib/execution-output.ts +13 -2
- package/lib/observability.ts +9 -11
- package/lib/registry.ts +89 -124
- package/lib/run-delivery.ts +130 -31
- package/lib/run-ui-runtime.ts +18 -34
- package/lib/runs-process-windows-mi.ts +167 -3
- package/lib/tools-inspect.ts +109 -59
- package/lib/tools-local.ts +26 -15
- package/lib/tools-message.ts +85 -58
- package/lib/tools-response.ts +97 -20
- package/lib/tools-spawn.ts +19 -12
- package/package.json +1 -1
- package/skills/actors/SKILL.md +1 -1
- package/skills/actors/references/diagnostics.md +3 -0
- package/skills/actors/references/runs.md +1 -1
package/lib/run-delivery.ts
CHANGED
|
@@ -69,7 +69,7 @@ export interface RunCompletionBatch {
|
|
|
69
69
|
/** Retained singleton capture; absence preserves older indexed bytes. Never a new-capture selector. */
|
|
70
70
|
readonly single_result?: true;
|
|
71
71
|
/** Captured record-list shape; no public format/version choice or source authority. */
|
|
72
|
-
readonly result_layout?: "records" | "run_headers" | "headings" | "headings_flat_omissions" | "headings_outcomes" | "headings_shared_results" | "headings_shared_kinds" | "frontmatter_v1" | "frontmatter_v2" | "completion_yaml_core_v1";
|
|
72
|
+
readonly result_layout?: "records" | "run_headers" | "headings" | "headings_flat_omissions" | "headings_outcomes" | "headings_shared_results" | "headings_shared_kinds" | "frontmatter_v1" | "frontmatter_v2" | "completion_yaml_core_v1" | "completion_yaml_files_v1";
|
|
73
73
|
/** Immutable scope/order/range data, not a caller-selected format or allocation authority. */
|
|
74
74
|
readonly numbering?: RunCompletionNumbering;
|
|
75
75
|
/** Formatting version/data only; no recipient ownership, read or publication capability. */
|
|
@@ -467,8 +467,12 @@ function isFrontmatterCompletionLayout(layout: unknown): boolean {
|
|
|
467
467
|
return layout === "frontmatter_v1" || layout === "frontmatter_v2";
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
function isYamlCompletionLayout(layout: unknown): boolean {
|
|
471
|
+
return layout === "completion_yaml_core_v1" || layout === "completion_yaml_files_v1";
|
|
472
|
+
}
|
|
473
|
+
|
|
470
474
|
function isSharedCompletionLayout(layout: unknown): boolean {
|
|
471
|
-
return layout === "headings_shared_results" || layout === "headings_shared_kinds" || layout
|
|
475
|
+
return layout === "headings_shared_results" || layout === "headings_shared_kinds" || isYamlCompletionLayout(layout) || isFrontmatterCompletionLayout(layout);
|
|
472
476
|
}
|
|
473
477
|
|
|
474
478
|
function parseBatch(value: unknown): RunCompletionBatch {
|
|
@@ -512,7 +516,7 @@ function parseBatch(value: unknown): RunCompletionBatch {
|
|
|
512
516
|
throw new Error("Completion shared text budget is invalid");
|
|
513
517
|
}
|
|
514
518
|
const numberingDescriptor = Object.getOwnPropertyDescriptor(record, "numbering");
|
|
515
|
-
if (numberingDescriptor && (resultLayout
|
|
519
|
+
if (numberingDescriptor && (isYamlCompletionLayout(resultLayout) || !Object.hasOwn(numberingDescriptor, "value") || presentationFormat !== "ax_records" || (resultLayout !== "headings" && resultLayout !== "headings_flat_omissions" && resultLayout !== "headings_outcomes" && !isSharedCompletionLayout(resultLayout)))) {
|
|
516
520
|
throw new Error("Completion numbering requires a canonical captured heading layout");
|
|
517
521
|
}
|
|
518
522
|
const numbering = numberingDescriptor ? parseCompletionNumbering(numberingDescriptor.value, members.length) : undefined;
|
|
@@ -891,7 +895,7 @@ export function requestRunCompletionStream(input: { tempDir: string; ownerId: st
|
|
|
891
895
|
}
|
|
892
896
|
|
|
893
897
|
export function admitRunCompletionBatch(input: AdmitRunCompletionBatchInput): RunCompletionBatch {
|
|
894
|
-
return captureCompletionBatch(input, false, "
|
|
898
|
+
return captureCompletionBatch(input, false, "completion_yaml_files_v1");
|
|
895
899
|
}
|
|
896
900
|
|
|
897
901
|
/** Kernel-only opt-in; ordinary Pi capture stays fenced until session/lifecycle integration is admitted. */
|
|
@@ -901,10 +905,10 @@ export function captureRunCompletionStreamBatch(input: AdmitRunCompletionBatchIn
|
|
|
901
905
|
|
|
902
906
|
/** Private ordinary candidate composition; stream capture retains its separately gated codec. */
|
|
903
907
|
export function captureRunCompletionYamlCandidateBatch(input: AdmitRunCompletionBatchInput): RunCompletionBatch {
|
|
904
|
-
return captureCompletionBatch(input, false, "
|
|
908
|
+
return captureCompletionBatch(input, false, "completion_yaml_files_v1");
|
|
905
909
|
}
|
|
906
910
|
|
|
907
|
-
function captureCompletionBatch(input: AdmitRunCompletionBatchInput, numbered: boolean, layout: "frontmatter_v2" | "completion_yaml_core_v1" = "frontmatter_v2"): RunCompletionBatch {
|
|
911
|
+
function captureCompletionBatch(input: AdmitRunCompletionBatchInput, numbered: boolean, layout: "frontmatter_v2" | "completion_yaml_core_v1" | "completion_yaml_files_v1" = "frontmatter_v2"): RunCompletionBatch {
|
|
908
912
|
const now = (input.now ?? new Date()).toISOString();
|
|
909
913
|
if (Object.hasOwn(input, "presentationFormat")) throw new Error("Completion presentation selection is not supported; new batches use AX.");
|
|
910
914
|
if (Object.hasOwn(input, "numbering")) throw new Error("Completion numbering is assigned only by journal capture");
|
|
@@ -917,7 +921,7 @@ function captureCompletionBatch(input: AdmitRunCompletionBatchInput, numbered: b
|
|
|
917
921
|
members: input.members,
|
|
918
922
|
phase: "pending",
|
|
919
923
|
});
|
|
920
|
-
const draft: RunCompletionBatch = layout
|
|
924
|
+
const draft: RunCompletionBatch = isYamlCompletionLayout(layout)
|
|
921
925
|
? parseBatch({ ...captured, result_layout: layout }) : { ...captured, result_layout: layout };
|
|
922
926
|
return mutateJournal(input.tempDir, input.ownerId, (journal) => {
|
|
923
927
|
let batch = draft;
|
|
@@ -946,9 +950,9 @@ function captureCompletionBatch(input: AdmitRunCompletionBatchInput, numbered: b
|
|
|
946
950
|
} else if (journal.completion_stream) {
|
|
947
951
|
throw new Error("Completion stream capture requires the numbered kernel path; stream state is retained");
|
|
948
952
|
}
|
|
949
|
-
if (layout
|
|
953
|
+
if (!isYamlCompletionLayout(layout)) batch = retainSharedCompletionTexts(batch);
|
|
950
954
|
if (projectCompletionBatchMessage(batch).membersShown === batch.members.length) {
|
|
951
|
-
if (layout
|
|
955
|
+
if (isYamlCompletionLayout(layout)) batch = retainSharedCompletionTexts(batch);
|
|
952
956
|
journal.completion_batch = batch;
|
|
953
957
|
return batch;
|
|
954
958
|
}
|
|
@@ -1515,7 +1519,7 @@ function isRoutineCompletionSummary(member: RunCompletionBatchMember): boolean {
|
|
|
1515
1519
|
}
|
|
1516
1520
|
|
|
1517
1521
|
function completionRecordRow(member: RunCompletionBatchMember, index: number, byIdentity: Map<string, RunCompletionBatchMember>, flat = false, layout?: RunCompletionBatch["result_layout"], textBudget?: number): CompletionRow {
|
|
1518
|
-
const frontmatter = isFrontmatterCompletionLayout(layout) || layout
|
|
1522
|
+
const frontmatter = isFrontmatterCompletionLayout(layout) || isYamlCompletionLayout(layout);
|
|
1519
1523
|
const simpleKinds = frontmatter || layout === "headings_shared_kinds", shared = isSharedCompletionLayout(layout);
|
|
1520
1524
|
const outcomes = shared || layout === "headings_outcomes", directOmission = outcomes || layout === "headings_flat_omissions";
|
|
1521
1525
|
const run = member.run.startsWith("run:") ? member.run.slice(4) : member.run;
|
|
@@ -1780,6 +1784,83 @@ function completionYamlReferences(row: CompletionRow): Record<string, Completion
|
|
|
1780
1784
|
return technical;
|
|
1781
1785
|
}
|
|
1782
1786
|
|
|
1787
|
+
/** Split selected paths without normalize/resolve/relative: dot and symlink routes must not be rewritten. */
|
|
1788
|
+
function rootedCompletionPath(path: CompletionYamlData, runRoot?: string): { root: string; path: string } | undefined {
|
|
1789
|
+
if (typeof path !== "string" || !path.isWellFormed()) return undefined;
|
|
1790
|
+
const windows = /^(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+[\\/])/u.test(path);
|
|
1791
|
+
const separator = windows ? /[\\/]/u : /\//u;
|
|
1792
|
+
if (!windows && !path.startsWith("/")) return undefined;
|
|
1793
|
+
if (path.split(separator).some(part => part === "." || part === "..")) return undefined;
|
|
1794
|
+
if (runRoot && path.startsWith(runRoot)) {
|
|
1795
|
+
const tail = path.slice(runRoot.length);
|
|
1796
|
+
if (separator.test(runRoot.slice(-1)) ? tail.length > 0 : separator.test(tail[0] ?? "")) {
|
|
1797
|
+
const relative = separator.test(runRoot.slice(-1)) ? tail : tail.slice(1);
|
|
1798
|
+
if (relative && !separator.test(relative[0]!)) return { root: runRoot, path: relative };
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
let split = -1;
|
|
1802
|
+
for (let i = 0; i < path.length; i++) if (separator.test(path[i]!)) split = i;
|
|
1803
|
+
if (split < 0 || split === path.length - 1) return undefined;
|
|
1804
|
+
const end = split === 0 || (windows && split === 2 && path[1] === ":") ? split + 1 : split;
|
|
1805
|
+
return { root: path.slice(0, end), path: path.slice(split + 1) };
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/** Body-free, read-free navigation over already selected references. Roots are presentation bases, not containment proof. */
|
|
1809
|
+
function completionRootedFiles(row: CompletionRow, member: RunCompletionBatchMember, locations?: RunDirectoryNavigation): Record<string, CompletionYamlData> {
|
|
1810
|
+
const technical = completionYamlReferences(row), files: Record<string, CompletionYamlData> = {};
|
|
1811
|
+
const groups: Array<{ root: CompletionYamlData; entries: Record<string, CompletionYamlData>[] }> = [];
|
|
1812
|
+
const runRoot = locations?.state === "available" ? locations.directory : undefined;
|
|
1813
|
+
const group = (root: CompletionYamlData): number => {
|
|
1814
|
+
const found = groups.findIndex(item => item.root === root);
|
|
1815
|
+
if (found >= 0) return found;
|
|
1816
|
+
groups.push({ root, entries: [] }); return groups.length - 1;
|
|
1817
|
+
};
|
|
1818
|
+
let priority = Infinity;
|
|
1819
|
+
const add = (path: CompletionYamlData, entry: Record<string, CompletionYamlData>, rank = Infinity, base?: string): void => {
|
|
1820
|
+
const selected = rootedCompletionPath(path, runRoot);
|
|
1821
|
+
const index = group(selected?.root ?? (base === undefined ? null : completionYamlString(base)));
|
|
1822
|
+
const selectedPath = selected?.path ?? path;
|
|
1823
|
+
groups[index]!.entries.push({ ...entry, path: selectedPath,
|
|
1824
|
+
...(!selected && base === undefined ? { path_basis: "as_recorded", base_unavailable: true } : {}) });
|
|
1825
|
+
if (selected && rank < priority) {
|
|
1826
|
+
priority = rank; files.read_first = { group: index, entry: groups[index]!.entries.length - 1 };
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
for (const [role, purpose, rank] of [
|
|
1830
|
+
["primary", "Full result text", 0], ["diagnostic_metadata", "Recorded error and command metadata", 1],
|
|
1831
|
+
["artifact_inventory", "Declared artifact inventory; target availability unproven", 2],
|
|
1832
|
+
] as const) {
|
|
1833
|
+
const detail = technical[role] as Record<string, CompletionYamlData> | undefined;
|
|
1834
|
+
if (!detail) continue;
|
|
1835
|
+
const { source: rawSource, ...facts } = detail;
|
|
1836
|
+
const { path, ...binding } = rawSource as Record<string, CompletionYamlData>;
|
|
1837
|
+
add(path!, { role, purpose, ...facts, source: binding }, rank);
|
|
1838
|
+
delete technical[role];
|
|
1839
|
+
}
|
|
1840
|
+
const artifacts = technical.artifacts as { scope: string; declarations: { name: CompletionYamlData; path: CompletionYamlData }[] } | undefined;
|
|
1841
|
+
if (artifacts) {
|
|
1842
|
+
for (const item of artifacts.declarations)
|
|
1843
|
+
add(item.path, { role: "artifact", purpose: "Declared artifact", name: item.name, scope: artifacts.scope, state: "declared" });
|
|
1844
|
+
delete technical.artifacts;
|
|
1845
|
+
}
|
|
1846
|
+
if (locations?.state === "available") {
|
|
1847
|
+
const { directory, files: selectedFiles, ...observation } = locations;
|
|
1848
|
+
files.observation = { ...observation, run: completionYamlString(locations.run), run_instance_id: completionYamlString(locations.run_instance_id),
|
|
1849
|
+
group: group(completionYamlString(directory)) };
|
|
1850
|
+
for (const item of selectedFiles) {
|
|
1851
|
+
const { path, ...facts } = item;
|
|
1852
|
+
const purpose = item.role === "result" ? "Recorded result metadata" : item.role === "execution" ? "Recorded execution and capture locations" : "Recorded Run file";
|
|
1853
|
+
add(typeof path === "string" ? completionYamlString(path) : null, { ...facts, purpose },
|
|
1854
|
+
item.state === "present" && item.role === "result" ? 3 : item.state === "present" && item.role === "execution" ? 4 : Infinity, directory);
|
|
1855
|
+
}
|
|
1856
|
+
} else if (locations) files.observation = { ...locations, run: completionYamlString(member.run), run_instance_id: completionYamlString(member.run_instance_id) };
|
|
1857
|
+
if (groups.length) {
|
|
1858
|
+
files.groups = groups;
|
|
1859
|
+
}
|
|
1860
|
+
if (Object.keys(technical).length) files.notes = technical;
|
|
1861
|
+
return files;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1783
1864
|
type PreparedCompletionYamlProjection =
|
|
1784
1865
|
| { state: "unsupported" }
|
|
1785
1866
|
| { state: "overbudget" }
|
|
@@ -1788,6 +1869,7 @@ type PreparedCompletionYamlProjection =
|
|
|
1788
1869
|
function prepareCompletionYamlProjection(safe: RunCompletionBatch, candidates: readonly { row: CompletionRow; location?: CompletionPiece }[],
|
|
1789
1870
|
references: ReadonlySet<number>, ordered: readonly RunCompletionBatchMember[]): PreparedCompletionYamlProjection {
|
|
1790
1871
|
const packet: CompletionYamlPacket = { batchId: safe.batch_id, records: [] };
|
|
1872
|
+
const rooted = safe.result_layout === "completion_yaml_files_v1";
|
|
1791
1873
|
if (!safe.batch_id.isWellFormed()) return { state: "unsupported" };
|
|
1792
1874
|
for (const [index, { row, location }] of candidates.entries()) {
|
|
1793
1875
|
const member = ordered[index]!;
|
|
@@ -1804,28 +1886,38 @@ function prepareCompletionYamlProjection(safe: RunCompletionBatch, candidates: r
|
|
|
1804
1886
|
else if (key === "artifact_declarations") metadata[key] = member.artifact_inventory?.declarations_total ?? Object.keys(member.artifacts ?? {}).length;
|
|
1805
1887
|
else metadata[key] = completionYamlString(value);
|
|
1806
1888
|
}
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
if (
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
: { ...
|
|
1889
|
+
const navigation = references.has(index) && location
|
|
1890
|
+
? safe.file_navigation!.find(item => safe.members[item.member_index] === member)!.locations : undefined;
|
|
1891
|
+
if (rooted) {
|
|
1892
|
+
const files = completionRootedFiles(row, member, navigation);
|
|
1893
|
+
if (Object.keys(files).length) metadata.files = files;
|
|
1894
|
+
} else {
|
|
1895
|
+
const technical = completionYamlReferences(row);
|
|
1896
|
+
if (Object.keys(technical).length) metadata.references = technical;
|
|
1897
|
+
if (navigation) {
|
|
1898
|
+
metadata.run_files = { run: completionYamlString(member.run), run_instance_id: completionYamlString(member.run_instance_id),
|
|
1899
|
+
locations: navigation.state === "available" ? { ...navigation, run: completionYamlString(navigation.run),
|
|
1900
|
+
run_instance_id: completionYamlString(navigation.run_instance_id), directory: completionYamlString(navigation.directory),
|
|
1901
|
+
files: navigation.files.map(file => ({ ...file, ...(typeof file.path === "string" ? { path: completionYamlString(file.path) } : {}) })) } as CompletionYamlData
|
|
1902
|
+
: { ...navigation } };
|
|
1903
|
+
}
|
|
1818
1904
|
}
|
|
1819
1905
|
const parts: CompletionYamlPart[] = [];
|
|
1820
1906
|
const add = (part: CompletionYamlPart) => { if (parts.length) parts.push({ marker: "\n" }); parts.push(part); };
|
|
1821
1907
|
for (const piece of row.pieces) if (piece.block) {
|
|
1822
1908
|
if (!piece.text.isWellFormed()) return { state: "unsupported" };
|
|
1823
|
-
|
|
1909
|
+
if (rooted && piece.block === "marker" && !/^\[[0-9]+ characters omitted\]$/u.test(piece.text)) {
|
|
1910
|
+
metadata.text_warnings ??= [];
|
|
1911
|
+
(metadata.text_warnings as CompletionYamlData[]).push(piece.text);
|
|
1912
|
+
} else add(piece.block === "body" ? { body: piece.text } : { marker: piece.text });
|
|
1913
|
+
}
|
|
1914
|
+
if (safe.file_navigation !== undefined && !references.has(index)) {
|
|
1915
|
+
if (rooted) metadata.run_files_omission = { reason: "packet_byte_limit", shown: 0, omitted: 1, total: 1 };
|
|
1916
|
+
else add({ marker: "Run-file reference omitted (packet byte limit)." });
|
|
1824
1917
|
}
|
|
1825
|
-
if (safe.file_navigation !== undefined && !references.has(index)) add({ marker: "Run-file reference omitted (packet byte limit)." });
|
|
1826
1918
|
packet.records.push({ metadata, parts });
|
|
1827
1919
|
}
|
|
1828
|
-
const service: Record<string, CompletionYamlData> = {};
|
|
1920
|
+
const service: Record<string, CompletionYamlData> = rooted ? { header_layout: "rooted_files_v1" } : {};
|
|
1829
1921
|
const omitted = safe.members.length - candidates.length;
|
|
1830
1922
|
if (omitted) {
|
|
1831
1923
|
service.omitted_results = omitted;
|
|
@@ -1841,7 +1933,11 @@ function prepareCompletionYamlProjection(safe: RunCompletionBatch, candidates: r
|
|
|
1841
1933
|
service.numbering = { ...safe.numbering, session_id: completionYamlString(safe.numbering.session_id), member_order: [...safe.numbering.member_order] };
|
|
1842
1934
|
if (safe.numbering.kind === "unavailable") service.numbering_unavailable = safe.numbering.reason;
|
|
1843
1935
|
}
|
|
1844
|
-
if (safe.inventory_source)
|
|
1936
|
+
if (safe.inventory_source) {
|
|
1937
|
+
const path = rooted ? rootedCompletionPath(safe.inventory_source.path) : undefined;
|
|
1938
|
+
service.result_inventory = { ...safe.inventory_source,
|
|
1939
|
+
...(path ? { root: path.root, path: path.path } : { path: completionYamlString(safe.inventory_source.path) }) };
|
|
1940
|
+
}
|
|
1845
1941
|
if (Object.keys(service).length) packet.service = service;
|
|
1846
1942
|
const chunks: string[] = [];
|
|
1847
1943
|
let bytes = 0;
|
|
@@ -1890,7 +1986,7 @@ export function projectRunCompletionYamlPrototype(batch: unknown):
|
|
|
1890
1986
|
}
|
|
1891
1987
|
}
|
|
1892
1988
|
|
|
1893
|
-
function projectCompletionRecords(safe: RunCompletionBatch, assembler: "legacy" | "yaml" = safe.result_layout
|
|
1989
|
+
function projectCompletionRecords(safe: RunCompletionBatch, assembler: "legacy" | "yaml" = isYamlCompletionLayout(safe.result_layout) ? "yaml" : "legacy"): { message: string; membersShown: number; compact_ranges: [number, number][]; textBudget?: number; blocks?: readonly CompletionBlock[] } {
|
|
1894
1990
|
const frontmatter = isFrontmatterCompletionLayout(safe.result_layout) || assembler === "yaml";
|
|
1895
1991
|
const shared = isSharedCompletionLayout(safe.result_layout);
|
|
1896
1992
|
const maxBytes = shared ? Limits.RUN_COMPLETION_MODEL_MAX_BYTES : Limits.RUN_DELIVERY_MODEL_MAX_BYTES;
|
|
@@ -2118,7 +2214,7 @@ export function projectRunCompletionBatchPresentation(batch: RunCompletionBatch)
|
|
|
2118
2214
|
const safe = parseBatch(batch);
|
|
2119
2215
|
if (safe.presentation_format !== "ax_records" && safe.presentation_format !== "ax_records_v3") return { content: projectCompletionBatchMessage(safe).message };
|
|
2120
2216
|
const result = safe.result_layout !== undefined ? projectCompletionRecords(safe) : projectRetainedRecordCompletionBatch(safe);
|
|
2121
|
-
if (safe.result_layout
|
|
2217
|
+
if (isYamlCompletionLayout(safe.result_layout)) return { content: result.message, presentation: Object.freeze({
|
|
2122
2218
|
format: "completion_yaml_core_v1", content_sha256: createHash("sha256").update(result.message).digest("hex"),
|
|
2123
2219
|
}) };
|
|
2124
2220
|
return { content: result.message, presentation: Object.freeze({ format: isFrontmatterCompletionLayout(safe.result_layout) ? "completion_frontmatter_v1" : safe.presentation_format, content_sha256: createHash("sha256").update(result.message).digest("hex"),
|
|
@@ -2253,19 +2349,22 @@ export function readRunCompletionPresentation(content: string, value: unknown):
|
|
|
2253
2349
|
!completionYamlKeys(hint, ["format", "content_sha256"]) || hint.format !== completionYamlCodec ||
|
|
2254
2350
|
hint.content_sha256 !== createHash("sha256").update(content).digest("hex")) return full;
|
|
2255
2351
|
const segments: NonNullable<RunCompletionPresentationView["segments"]>[number][] = [];
|
|
2352
|
+
const rooted = core.packet.service?.header_layout === "rooted_files_v1";
|
|
2353
|
+
const facts = Object.fromEntries(Object.entries(core.packet.service ?? {}).filter(([key]) => key !== "header_layout"));
|
|
2256
2354
|
for (const [index, record] of core.packet.records.entries()) {
|
|
2257
2355
|
if (index) segments.push({ kind: "separator", text: "\n" });
|
|
2258
|
-
const metadata = Object.fromEntries(Object.entries(record.metadata).filter(([key]) => key !== "references" && key !== "run_files"));
|
|
2356
|
+
const metadata = Object.fromEntries(Object.entries(record.metadata).filter(([key]) => rooted || (key !== "references" && key !== "run_files")));
|
|
2357
|
+
if (rooted && index === 0 && Object.keys(facts).length) metadata.delivery = facts;
|
|
2259
2358
|
const header = renderYamlMetadata(metadata, completionYamlMaxBytes);
|
|
2260
2359
|
if (header.state !== "available") return full;
|
|
2261
2360
|
segments.push({ kind: "header", text: record.parts.length ? `---\n${header.text}---` : header.text.replace(/\n$/u, "") });
|
|
2262
2361
|
for (const part of record.parts) segments.push("body" in part ? { kind: "body", text: part.body } : { kind: "marker", text: part.marker });
|
|
2263
2362
|
}
|
|
2264
|
-
if (core.packet.service) {
|
|
2265
|
-
const service = renderYamlMetadata(core.packet.service, completionYamlMaxBytes);
|
|
2363
|
+
if (core.packet.service && (!rooted || !core.packet.records.length)) {
|
|
2364
|
+
const service = renderYamlMetadata(rooted ? facts : core.packet.service, completionYamlMaxBytes);
|
|
2266
2365
|
if (service.state !== "available") return full;
|
|
2267
2366
|
if (segments.length) segments.push({ kind: "separator", text: "\n" });
|
|
2268
|
-
segments.push({ kind: "service", text: service.text.replace(/\n$/u, "") });
|
|
2367
|
+
segments.push({ kind: rooted ? "header" : "service", text: service.text.replace(/\n$/u, "") });
|
|
2269
2368
|
}
|
|
2270
2369
|
return { text: segments.map(segment => segment.text).join("\n"), compact: true, records: true, yaml: true,
|
|
2271
2370
|
segments: Object.freeze(segments.map(segment => Object.freeze(segment))) };
|
package/lib/run-ui-runtime.ts
CHANGED
|
@@ -14,7 +14,7 @@ import * as Pi from "./pi.ts";
|
|
|
14
14
|
import * as RunDelivery from "./run-delivery.ts";
|
|
15
15
|
import * as RunDeliveryLineage from "./run-delivery-lineage.ts";
|
|
16
16
|
import * as RunIndex from "./runs-index.ts";
|
|
17
|
-
import { formatLiteralBlock,
|
|
17
|
+
import { formatLiteralBlock, formatYamlNotice } from "./tools-response.ts";
|
|
18
18
|
|
|
19
19
|
export interface RunUiRuntime {
|
|
20
20
|
close(): void;
|
|
@@ -48,21 +48,18 @@ export interface RunUiRuntimeDeps {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function formatSteerDiagnosticMessage(eventId: string, stage: "admission" | "delivery", message: string): string {
|
|
51
|
-
const fields = [formatMarkdownField("event", eventId), formatMarkdownField("attention", "steer"),
|
|
52
|
-
formatMarkdownField("stage", stage), formatMarkdownField("result", stage === "admission" ? "remains retryable" : "request failed")];
|
|
53
|
-
if (stage === "delivery") fields.push(formatMarkdownField("delivery", "unconfirmed"), formatMarkdownField("presentation", "unconfirmed"));
|
|
54
51
|
let end = Math.min(message.length, 240);
|
|
55
52
|
const last = message.charCodeAt(end - 1), next = message.charCodeAt(end);
|
|
56
53
|
if (last >= 0xd800 && last <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) end -= 1;
|
|
57
54
|
const preview = message.slice(0, end), clipped = end < message.length;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
// The 240-pair-aligned preview and its explicit unknown-loss marker are unchanged; only metadata is YAML.
|
|
56
|
+
const diagnostic: Record<string, string> = Buffer.from(preview).toString("utf8") !== preview
|
|
57
|
+
? { diagnostic: "unavailable", diagnostic_reason: "malformed legacy preview", diagnostic_omission: "[omitted: unknown]" }
|
|
58
|
+
: { diagnostic: preview + (clipped ? "…" : ""), ...(clipped ? { diagnostic_omission: "[omitted: unknown]" } : {}) };
|
|
59
|
+
return formatYamlNotice({ event: eventId, attention: "steer", stage,
|
|
60
|
+
result: stage === "admission" ? "remains retryable" : "request failed",
|
|
61
|
+
...(stage === "delivery" ? { delivery: "unconfirmed", presentation: "unconfirmed" } : {}),
|
|
62
|
+
...diagnostic });
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
/** Transcript-only view of the exact delivered content; never re-read a Run or infer body labels. */
|
|
@@ -184,8 +181,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
184
181
|
}
|
|
185
182
|
const message = error instanceof Error ? error.message : String(error);
|
|
186
183
|
try {
|
|
187
|
-
expectedContext.ui.notify(
|
|
188
|
-
formatMarkdownField("diagnostic", message)].join("\n"), "error");
|
|
184
|
+
expectedContext.ui.notify(formatYamlNotice({ callback: label, result: "failed", diagnostic: message }), "error");
|
|
189
185
|
} catch {
|
|
190
186
|
/* stale context or unavailable UI */
|
|
191
187
|
}
|
|
@@ -369,12 +365,8 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
369
365
|
const key = `${batch.batch_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
370
366
|
if (!completionDiagnostics.has(key)) {
|
|
371
367
|
completionDiagnostics.add(key);
|
|
372
|
-
ctx.ui.notify(
|
|
373
|
-
|
|
374
|
-
formatMarkdownField("evidence", evidence.status), formatMarkdownField("phase", "pending"),
|
|
375
|
-
formatMarkdownField("reason", evidence.reason ?? "conflicting session evidence")].join("\n"),
|
|
376
|
-
"warning",
|
|
377
|
-
);
|
|
368
|
+
ctx.ui.notify("\n" + formatYamlNotice({ batch: batch.batch_id, stage: "completion recovery",
|
|
369
|
+
evidence: evidence.status, phase: "pending", reason: evidence.reason ?? "conflicting session evidence" }), "warning");
|
|
378
370
|
}
|
|
379
371
|
return true;
|
|
380
372
|
}
|
|
@@ -440,12 +432,8 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
440
432
|
const key = `${steer.steer_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
441
433
|
if (!steerDiagnostics.has(key)) {
|
|
442
434
|
steerDiagnostics.add(key);
|
|
443
|
-
ctx.ui.notify(
|
|
444
|
-
|
|
445
|
-
formatMarkdownField("stage", "recovery"), formatMarkdownField("evidence", evidence.status),
|
|
446
|
-
formatMarkdownField("phase", "queued"), formatMarkdownField("reason", evidence.reason ?? "conflicting session evidence")].join("\n"),
|
|
447
|
-
"warning",
|
|
448
|
-
);
|
|
435
|
+
ctx.ui.notify(formatYamlNotice({ event: steer.event_id, attention: "steer", stage: "recovery",
|
|
436
|
+
evidence: evidence.status, phase: "queued", reason: evidence.reason ?? "conflicting session evidence" }), "warning");
|
|
449
437
|
}
|
|
450
438
|
continue;
|
|
451
439
|
}
|
|
@@ -714,14 +702,10 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
714
702
|
);
|
|
715
703
|
if (teardown.failed === 0 || !ctx) return;
|
|
716
704
|
try {
|
|
717
|
-
ctx.ui.notify(
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
formatMarkdownField("summary", teardown.summaryPath == null ? "unavailable" : formatLiteralBlock(teardown.summaryPath)),
|
|
722
|
-
formatMarkdownField("basis", "teardown report; not scope closure")].join("\n"),
|
|
723
|
-
"warning",
|
|
724
|
-
);
|
|
705
|
+
ctx.ui.notify(formatYamlNotice({ stage: "shutdown teardown", killed: teardown.killed,
|
|
706
|
+
failed: teardown.failed, skipped: teardown.skipped, discovery_failed: teardown.discoveryFailed,
|
|
707
|
+
summary: teardown.summaryPath ?? "unavailable",
|
|
708
|
+
basis: "teardown report; not scope closure" }), "warning");
|
|
725
709
|
} catch {
|
|
726
710
|
/* stale shutdown context */
|
|
727
711
|
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
// Owns: Bounded raw MI wire decoding, query inputs
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Owns: Bounded raw MI wire decoding, query inputs, unadmitted provenance fragments, and unrouted
|
|
2
|
+
// helper manifest/asset verification plus a shell-free invocation spec, subordinate to process acquisition.
|
|
3
|
+
// Excludes: Identity construction/digest/comparison, status/liveness/lifecycle/publication/admission, fallback,
|
|
4
|
+
// process spawning and package inventory. No production reader imports this owner; decoded data, verified
|
|
5
|
+
// asset bytes and the invocation spec never admit a representation profile, architecture or activation.
|
|
6
|
+
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
const MANIFEST_MAX_BYTES = 32 * 1024;
|
|
10
|
+
const EXECUTABLE_MAX_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
const HELPER_VERSION = /^(?:0|[1-9][0-9]{0,5})\.(?:0|[1-9][0-9]{0,5})\.(?:0|[1-9][0-9]{0,5})$/u;
|
|
12
|
+
const MODULE_FILE = /\/lib\/runs-process-windows-mi\.(?:ts|js)$/u;
|
|
4
13
|
|
|
5
14
|
type NativeStage = 'application' | 'session' | 'input' | 'retrieval' | 'operation' | 'representation' | 'cleanup';
|
|
6
15
|
type RawTimestamp = {
|
|
@@ -111,3 +120,158 @@ export function decodeResponse(bytes: Uint8Array, profile: unknown, pids: unknow
|
|
|
111
120
|
if (optionalNull) return { kind: 'unusable', reason: 'unsupported_optional_fields' };
|
|
112
121
|
return { kind: 'decoded_unadmitted', rows };
|
|
113
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Complete-helper manifest claim. Bounded reading, exact-key schema and closed lexemes only; a valid
|
|
126
|
+
* manifest is integrity/provenance input, never executable authenticity, an approved asset or activation.
|
|
127
|
+
*/
|
|
128
|
+
type HelperManifest = {
|
|
129
|
+
readonly helper_version: string;
|
|
130
|
+
readonly architecture: 'windows-x64';
|
|
131
|
+
readonly protocol: 'native-mi-v1';
|
|
132
|
+
readonly profiles: readonly ['scalar', 'shared'];
|
|
133
|
+
readonly native_source_sha256: string;
|
|
134
|
+
readonly executable_bytes: number;
|
|
135
|
+
readonly executable_sha256: string;
|
|
136
|
+
readonly toolchain: { readonly compiler: string; readonly sdk: string; readonly build_arguments: readonly string[] };
|
|
137
|
+
readonly dependencies: readonly string[];
|
|
138
|
+
readonly originating_source_commit: string;
|
|
139
|
+
readonly originating_build_job: string;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type ManifestUnusable =
|
|
143
|
+
| { readonly kind: 'unusable'; readonly reason: 'oversize_manifest' | 'invalid_manifest' | 'unsupported_manifest' };
|
|
144
|
+
|
|
145
|
+
function boundedText(bytes: Uint8Array, maximumBytes: number): string | undefined {
|
|
146
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength > maximumBytes) return undefined;
|
|
147
|
+
if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) return undefined;
|
|
148
|
+
try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); }
|
|
149
|
+
catch { return undefined; }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function boundedLexemes(value: unknown, maximum: number): string[] | undefined {
|
|
153
|
+
if (!Array.isArray(value) || value.length > maximum) return undefined;
|
|
154
|
+
const selected: string[] = [];
|
|
155
|
+
for (const item of value) {
|
|
156
|
+
if (typeof item !== 'string' || item.length === 0 || item.length > 128 || !/^[\x20-\x7e]+$/.test(item) || selected.includes(item)) return undefined;
|
|
157
|
+
selected.push(item);
|
|
158
|
+
}
|
|
159
|
+
return selected;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Bounded complete-manifest reader: one strict UTF-8 JSON document with an exact reviewed shape. */
|
|
163
|
+
export function readHelperManifest(bytes: Uint8Array):
|
|
164
|
+
| { readonly kind: 'manifest_unadmitted'; readonly manifest: HelperManifest }
|
|
165
|
+
| ManifestUnusable {
|
|
166
|
+
const invalid: ManifestUnusable = { kind: 'unusable', reason: 'invalid_manifest' };
|
|
167
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength > MANIFEST_MAX_BYTES) return { kind: 'unusable', reason: 'oversize_manifest' };
|
|
168
|
+
const text = boundedText(bytes, MANIFEST_MAX_BYTES);
|
|
169
|
+
if (text === undefined) return invalid;
|
|
170
|
+
let value: unknown;
|
|
171
|
+
try { value = JSON.parse(text); }
|
|
172
|
+
catch { return invalid; }
|
|
173
|
+
if (!exactKeys(value, ['helper_version', 'architecture', 'protocol', 'profiles', 'native_source_sha256',
|
|
174
|
+
'executable_bytes', 'executable_sha256', 'toolchain', 'dependencies', 'originating_source_commit', 'originating_build_job']))
|
|
175
|
+
return invalid;
|
|
176
|
+
const { helper_version, architecture, protocol, profiles, native_source_sha256, executable_bytes,
|
|
177
|
+
executable_sha256, toolchain, dependencies, originating_source_commit, originating_build_job } = value;
|
|
178
|
+
if (typeof helper_version !== 'string' || !HELPER_VERSION.test(helper_version) ||
|
|
179
|
+
architecture !== 'windows-x64' || protocol !== 'native-mi-v1' ||
|
|
180
|
+
!Array.isArray(profiles) || profiles.length !== 2 || profiles[0] !== 'scalar' || profiles[1] !== 'shared')
|
|
181
|
+
return { kind: 'unusable', reason: 'unsupported_manifest' };
|
|
182
|
+
if (typeof native_source_sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(native_source_sha256) ||
|
|
183
|
+
typeof executable_sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(executable_sha256) ||
|
|
184
|
+
typeof originating_source_commit !== 'string' || !/^[0-9a-f]{40}$/.test(originating_source_commit) ||
|
|
185
|
+
typeof originating_build_job !== 'string' || !/^[1-9][0-9]*$/.test(originating_build_job) ||
|
|
186
|
+
typeof executable_bytes !== 'number' || !Number.isSafeInteger(executable_bytes) || executable_bytes <= 0)
|
|
187
|
+
return invalid;
|
|
188
|
+
if (executable_bytes > EXECUTABLE_MAX_BYTES) return { kind: 'unusable', reason: 'unsupported_manifest' };
|
|
189
|
+
if (!exactKeys(toolchain, ['compiler', 'sdk', 'build_arguments'])) return invalid;
|
|
190
|
+
const compiler = boundedLexemes([toolchain.compiler], 1)?.[0];
|
|
191
|
+
const sdk = boundedLexemes([toolchain.sdk], 1)?.[0];
|
|
192
|
+
const buildArguments = boundedLexemes(toolchain.build_arguments, 32);
|
|
193
|
+
const dependencyNames = boundedLexemes(dependencies, 32);
|
|
194
|
+
if (compiler === undefined || sdk === undefined || buildArguments === undefined || dependencyNames === undefined) return invalid;
|
|
195
|
+
// Fresh primitive claims only; the caller still owns target/version/source-digest policy.
|
|
196
|
+
return {
|
|
197
|
+
kind: 'manifest_unadmitted',
|
|
198
|
+
manifest: {
|
|
199
|
+
helper_version, architecture: 'windows-x64', protocol: 'native-mi-v1', profiles: ['scalar', 'shared'],
|
|
200
|
+
native_source_sha256, executable_bytes, executable_sha256,
|
|
201
|
+
toolchain: { compiler, sdk, build_arguments: buildArguments }, dependencies: dependencyNames,
|
|
202
|
+
originating_source_commit, originating_build_job,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
type HelperAssetLocation = {
|
|
208
|
+
readonly kind: 'located';
|
|
209
|
+
readonly helper_version: string;
|
|
210
|
+
readonly manifest_url: URL;
|
|
211
|
+
readonly executable_url: URL;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Fixed versioned package-relative lookup. Never searches cwd, PATH or caller locations: the only base is
|
|
216
|
+
* the sibling `native/` directory of this module's own `lib/` (source) or `dist/lib/` (compiled) home.
|
|
217
|
+
*/
|
|
218
|
+
export function resolveHelperAsset(moduleUrl: unknown, runtime: unknown, helperVersion: unknown):
|
|
219
|
+
| HelperAssetLocation
|
|
220
|
+
| { readonly kind: 'unusable'; readonly reason: 'unsupported_location' | 'unsupported_target' | 'unsupported_version' } {
|
|
221
|
+
let url: URL;
|
|
222
|
+
try { url = new URL(String(moduleUrl)); }
|
|
223
|
+
catch { return { kind: 'unusable', reason: 'unsupported_location' }; }
|
|
224
|
+
if (url.protocol !== 'file:' || !MODULE_FILE.test(url.pathname)) return { kind: 'unusable', reason: 'unsupported_location' };
|
|
225
|
+
const target = runtime as { platform?: unknown; arch?: unknown } | null | undefined;
|
|
226
|
+
if (target?.platform !== 'win32' || target?.arch !== 'x64') return { kind: 'unusable', reason: 'unsupported_target' };
|
|
227
|
+
if (typeof helperVersion !== 'string' || !HELPER_VERSION.test(helperVersion)) return { kind: 'unusable', reason: 'unsupported_version' };
|
|
228
|
+
const directory = `../native/windows-mi/win32-x64/${helperVersion}/`;
|
|
229
|
+
return {
|
|
230
|
+
kind: 'located', helper_version: helperVersion,
|
|
231
|
+
manifest_url: new URL(`${directory}manifest.json`, url),
|
|
232
|
+
executable_url: new URL(`${directory}helper.exe`, url),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Injected bounded reader: exact bytes for one fixed URL or nothing. Never a path, cwd or PATH lookup. */
|
|
237
|
+
export type HelperAssetReader = (url: URL, maximumBytes: number) => Uint8Array | undefined;
|
|
238
|
+
|
|
239
|
+
type HelperAssetOutcome =
|
|
240
|
+
| { readonly kind: 'asset_verified_unadmitted'; readonly manifest: HelperManifest; readonly executable_sha256: string; readonly executable_bytes: number }
|
|
241
|
+
| { readonly kind: 'unusable'; readonly reason:
|
|
242
|
+
'missing_manifest' | 'oversize_manifest' | 'invalid_manifest' | 'unsupported_manifest' | 'version_mismatch' |
|
|
243
|
+
'missing_asset' | 'oversize_asset' | 'size_mismatch' | 'digest_mismatch' };
|
|
244
|
+
|
|
245
|
+
/** Trusted exact-byte selection: bounded manifest read, exact declared size, then exact SHA-256 match. */
|
|
246
|
+
export function loadHelperAsset(location: HelperAssetLocation, read: HelperAssetReader): HelperAssetOutcome {
|
|
247
|
+
const manifestBytes = read(location.manifest_url, MANIFEST_MAX_BYTES + 1);
|
|
248
|
+
if (manifestBytes === undefined) return { kind: 'unusable', reason: 'missing_manifest' };
|
|
249
|
+
if (manifestBytes.byteLength > MANIFEST_MAX_BYTES) return { kind: 'unusable', reason: 'oversize_manifest' };
|
|
250
|
+
const reading = readHelperManifest(manifestBytes);
|
|
251
|
+
if (reading.kind === 'unusable') return { kind: 'unusable', reason: reading.reason };
|
|
252
|
+
const { manifest } = reading;
|
|
253
|
+
if (manifest.helper_version !== location.helper_version) return { kind: 'unusable', reason: 'version_mismatch' };
|
|
254
|
+
// One extra byte detects growth; a shorter read can never satisfy the declared size.
|
|
255
|
+
const executableBytes = read(location.executable_url, manifest.executable_bytes + 1);
|
|
256
|
+
if (executableBytes === undefined) return { kind: 'unusable', reason: 'missing_asset' };
|
|
257
|
+
if (executableBytes.byteLength > manifest.executable_bytes) return { kind: 'unusable', reason: 'oversize_asset' };
|
|
258
|
+
if (executableBytes.byteLength !== manifest.executable_bytes) return { kind: 'unusable', reason: 'size_mismatch' };
|
|
259
|
+
const digest = createHash('sha256').update(executableBytes).digest('hex');
|
|
260
|
+
if (digest !== manifest.executable_sha256) return { kind: 'unusable', reason: 'digest_mismatch' };
|
|
261
|
+
return { kind: 'asset_verified_unadmitted', manifest, executable_sha256: digest, executable_bytes: executableBytes.byteLength };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
type HelperInvocation =
|
|
265
|
+
| { readonly kind: 'invocation_unadmitted'; readonly command: string; readonly args: readonly string[];
|
|
266
|
+
readonly options: { readonly shell: false; readonly windowsHide: true } }
|
|
267
|
+
| { readonly kind: 'unusable'; readonly reason: 'unsupported_executable' | 'invalid_query' };
|
|
268
|
+
|
|
269
|
+
/** Shell-free invocation spec only: no spawn, retry, fallback or execution-policy bypass. */
|
|
270
|
+
export function helperInvocation(executablePath: unknown, profile: unknown, pids: unknown): HelperInvocation {
|
|
271
|
+
if (typeof executablePath !== 'string' || executablePath.length === 0 || executablePath.length > 4096 ||
|
|
272
|
+
!/^[A-Za-z]:[\\/][^\x00\r\n"]*\.exe$/.test(executablePath)) return { kind: 'unusable', reason: 'unsupported_executable' };
|
|
273
|
+
const query = queryArguments(profile, pids);
|
|
274
|
+
if (!query) return { kind: 'unusable', reason: 'invalid_query' };
|
|
275
|
+
return { kind: 'invocation_unadmitted', command: executablePath, args: query,
|
|
276
|
+
options: { shell: false, windowsHide: true } };
|
|
277
|
+
}
|