hoi4-agent-tools 3.0.9 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -1
- package/README.md +10 -2
- package/dist/bin/stdio.js +23 -12
- package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
- package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
- package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
- package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -0
- package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
- package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
- package/dist/hoi4_agent_tools/core/engine.js +48 -5
- package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
- package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
- package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
- package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
- package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
- package/dist/hoi4_agent_tools/core/index.js +47 -4
- package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
- package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
- package/dist/hoi4_agent_tools/core/job-service.js +112 -0
- package/dist/hoi4_agent_tools/core/job-store.js +641 -0
- package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +202 -0
- package/dist/hoi4_agent_tools/core/job-worker.js +64 -0
- package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
- package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
- package/dist/hoi4_agent_tools/core/operation-tasks.js +387 -0
- package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
- package/dist/hoi4_agent_tools/core/result.js +1 -0
- package/dist/hoi4_agent_tools/core/scanner.js +15 -75
- package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
- package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
- package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
- package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
- package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
- package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
- package/dist/hoi4_agent_tools/core/transactions.js +80 -27
- package/dist/hoi4_agent_tools/decision/ai.js +104 -0
- package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
- package/dist/hoi4_agent_tools/decision/cost.js +177 -0
- package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
- package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
- package/dist/hoi4_agent_tools/decision/service.js +172 -0
- package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
- package/dist/hoi4_agent_tools/event/graph.js +214 -290
- package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
- package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
- package/dist/hoi4_agent_tools/event/operations.js +179 -0
- package/dist/hoi4_agent_tools/event/service.js +67 -15
- package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
- package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
- package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
- package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
- package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
- package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
- package/dist/hoi4_agent_tools/impact/service.js +127 -0
- package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
- package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
- package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
- package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
- package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
- package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
- package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +18 -6
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
- package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
- package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +45 -0
- package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
- package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +16 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
- package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
- package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
- package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
- package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
- package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
- package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
- package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
- package/dist/hoi4_agent_tools/probability/operations.js +183 -0
- package/dist/hoi4_agent_tools/probability/service.js +24 -3
- package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
- package/dist/hoi4_agent_tools/schemas/common.js +2 -0
- package/dist/hoi4_agent_tools/schemas/event.js +72 -1
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
- package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
- package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
- package/dist/hoi4_agent_tools/technology/graph.js +83 -78
- package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
- package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
- package/dist/hoi4_agent_tools/technology/operations.js +125 -0
- package/dist/hoi4_agent_tools/technology/service.js +89 -15
- package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +3 -0
- package/docs/analysis.md +100 -0
- package/docs/development.md +2 -0
- package/docs/events.md +7 -2
- package/docs/helper-expansion.md +78 -0
- package/docs/http.md +2 -0
- package/docs/jobs.md +86 -0
- package/docs/technology.md +6 -2
- package/package.json +15 -6
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +290 -0
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +44 -0
- package/schemas/helper-expansion-summary.schema.json +70 -0
- package/schemas/impact-inspect-input.schema.json +203 -0
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 3.2.0 - 2026-09-20
|
|
6
|
+
|
|
7
|
+
- Add read-only `hoi4.impact_inspect` with active and overridden definitions, direct and transitive source consumers, state-key access roles, cycle and coverage evidence, and in-memory source comparisons.
|
|
8
|
+
- Connect event, focus, decision, idea, technology, scripted-helper, localisation, scripted-GUI, GUI, sprite, and texture references where typed static source evidence supports an edge; leave dynamic and unsupported references unresolved.
|
|
9
|
+
- Identify affected declared scenario-suite cases from bounded workspace-owned JSON references without executing them.
|
|
10
|
+
- Add read-only `hoi4.decision_inspect` for decision and mission inventory, declared actor and target gates, costs and direct payments, lifecycle paths, and scenario-by-scenario source comparisons.
|
|
11
|
+
- Reuse the probability adapters for decision and mission AI scores and score comparisons, retaining linked traces and explicit proposal limits.
|
|
12
|
+
- Serve both tools through ordinary and persistent native MCP tasks over legacy and modern stdio and HTTP routes, with revision-bound linked reports and source-preserving overlays.
|
|
13
|
+
|
|
14
|
+
## 3.1.0 - 2026-09-20
|
|
15
|
+
|
|
16
|
+
- Reuse authenticated content-addressed parsed documents and file-local index segments across authorized local processes while revalidating current source bytes, load order, shadowing, workspace topology, and principal scope.
|
|
17
|
+
- Rebuild exact indexes from immutable segments, retain typed reverse source dependencies, and heal malformed or tampered persistent cache entries without trusting timestamps or file sizes.
|
|
18
|
+
- Run event, technology, probability, map, GUI, and focus analysis in fixed-entry isolated workers backed by durable authenticated jobs, revision-fenced ownership, bounded retention, and source-revision result checkpoints.
|
|
19
|
+
- Advertise optional negotiated MCP tasks for all long-running domain calls while preserving exact ordinary-call results, progress, cancellation, linked resources, tool errors, and both stdio and HTTP compatibility.
|
|
20
|
+
- Share task execution, authorization, and all 23 domain tool definitions with an MCP 2026-07-28 Tasks extension adapter, routed by production stdio and authenticated HTTP entry points.
|
|
21
|
+
- Share `hoi4.job_inspect` and `hoi4.job_cancel` between protocol adapters as responsive, principal-private compatibility controls that remain available while domain execution slots are occupied.
|
|
22
|
+
- Share the bounded probability-analysis prompt metadata and body between protocol adapters without starting jobs or source analysis.
|
|
23
|
+
- Advertise the same server instructions through legacy initialization and modern discovery.
|
|
24
|
+
- Deliver opt-in modern request progress through the SDK's related-notification API, preserving ordered heartbeats and cancellation over connection and auto-streaming production HTTP serving.
|
|
25
|
+
- Pin each bounded stdio connection to its opening protocol era and route authenticated modern HTTP claims through strict SDK validation without changing legacy stateful sessions or the existing request-security gates.
|
|
26
|
+
- Share the two opt-in ChaosX private-tool implementations between stdio protocol generations while keeping them absent from ordinary and HTTP catalogs.
|
|
27
|
+
- Pin patched Sharp, Hono, Vitest, and coverage packages while retaining the exact dependency graph in the lockfile.
|
|
28
|
+
- Require caller-stable request keys for native background focus, GUI, and map rewrites; deduplicate exact retries independently of polling hints and reject conflicting reuse.
|
|
29
|
+
- Derive new job IDs from private server state, preserve authenticated earlier retry receipts without replay, and authenticate modern task handles before execution admission or cancellation.
|
|
30
|
+
- Renew only task-visibility metadata for exact retries of expired rewrite receipts, preserving terminal outcomes and source edits through native and ordinary calls in both protocol generations.
|
|
31
|
+
- Share bounded artifact-resource reads between protocol generations, including exact byte ranges, continuation links, manifest projection, and workspace-grant checks.
|
|
32
|
+
- Bind rewrite transactions and domain completion recipes before source application, reconcile stopped workers without replanning, and retain post-write validation artifacts without changing immutable plan hashes.
|
|
33
|
+
- Retain revision-addressed event and technology graphs for comparisons after process restart, and preserve explicit helper, source, candidate, node, edge, and render coverage boundaries.
|
|
34
|
+
- Add opt-in bounded `helper_expansion` pages for event and technology inspection, preserving distinct source call paths and resumable principal- and revision-bound cursors without retaining completed traversal history.
|
|
35
|
+
- Mark depth- and projection-truncated materialized helper graphs incomplete, and retain explicit cycle, dynamic-reference, source-coverage, and page-retention boundaries.
|
|
36
|
+
- Harden Windows atomic job publication, bounded pre-dispatch worker-lease transfer retries, disconnect recovery, principal isolation, task expiry, and mutation-receipt retention under concurrent clients.
|
|
6
37
|
|
|
7
38
|
## 3.0.9 - 2026-09-08
|
|
8
39
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# HOI4 Agent Tools
|
|
2
2
|
|
|
3
|
-
HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron IV event chains, technology systems, AI weights, and MTTH timing and to inspect, create, or clean up focus trees, scripted GUIs, and maps. It produces source-linked structural and visual evidence that agents can use inside a larger modding workflow.
|
|
3
|
+
HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron IV event chains, decisions, cross-system dependencies, technology systems, AI weights, and MTTH timing and to inspect, create, or clean up focus trees, scripted GUIs, and maps. It produces source-linked structural and visual evidence that agents can use inside a larger modding workflow.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
@@ -8,6 +8,7 @@ HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron
|
|
|
8
8
|
- Scripted GUIs: trace GUI, GFX, scripted-GUI, and localisation links; render value-driven variants, dynamic country flags, inline text icons, states, resolutions, hierarchy, and click regions; diagnose alignment, visibility, clipping, panel containment, and button-label centering; create or repair interface source.
|
|
9
9
|
- Maps: navigate the complete rendered map by ID or localised name; inspect provinces, states, regions, adjacency, supply, railways, and positions; create states and provinces; change IDs; and repair connected map data.
|
|
10
10
|
- Event chains: scan definitions and call sites, trace routes and state flow, lint references, render graphs, and compare revisions without editing event source.
|
|
11
|
+
- Cross-system impact and decisions: trace definitions and consumers across source systems, evaluate declared actor and target scenarios, inspect costs and mission paths, and compare in-memory proposals.
|
|
11
12
|
- Technology trees: reconstruct technology and doctrine paths, folder layouts, unlocks, bonuses, grants, metadata, assets, and structural changes.
|
|
12
13
|
- AI and MTTH: evaluate weighted choices and timing across explicit scenarios, bind special scope chains, enumerate dynamic target pools, sweep uncertain inputs, simulate distributions, compare patches, and analyze declared stateful pools.
|
|
13
14
|
|
|
@@ -71,6 +72,10 @@ On non-Windows systems, use `hoi4-agent-tools` as the command. Agentic HOI4 repo
|
|
|
71
72
|
| `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
|
|
72
73
|
| `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
|
|
73
74
|
| `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
|
|
75
|
+
| `hoi4.impact_inspect` | Trace symbol and changed-file consumers across source systems and compare proposals. |
|
|
76
|
+
| `hoi4.decision_inspect` | Inventory and evaluate decisions or missions under declared scenarios and source changes. |
|
|
77
|
+
| `hoi4.job_inspect` | Inspect durable background work or retrieve its completed tool result. |
|
|
78
|
+
| `hoi4.job_cancel` | Durably request cancellation of authorized background work. |
|
|
74
79
|
| `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
|
|
75
80
|
| `hoi4.tech_render` | Render source layouts with real item sizes and year guides, plus dependencies and assets. |
|
|
76
81
|
| `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
|
|
@@ -86,11 +91,13 @@ Set `HOI4_AGENT_TOOLS_CHAOSX=1` on the server process to expose the optional `ch
|
|
|
86
91
|
|
|
87
92
|
Large outputs are linked `hoi4-agent://` resources. For resources over 1 MiB, follow the `continuationUri` returned in `_meta` until it is `null`; clients may also request byte ranges with `?offset=<bytes>&length=<bytes>`.
|
|
88
93
|
|
|
94
|
+
Long-running domain calls advertise optional MCP task support. A task survives client disconnects and can be retrieved after reconnect; clients without task support keep the ordinary synchronous result contract. Native background rewrites require a stable caller-supplied `requestKey` so retries resolve to one durable mutation receipt. See [Persistent jobs and MCP tasks](docs/jobs.md).
|
|
95
|
+
|
|
89
96
|
## Coexistence with agent workflows
|
|
90
97
|
|
|
91
98
|
HOI4 Agent Tools provides HOI4 domain operations without replacing repository instructions such as `AGENTS.md`, skills, plans, or subagents. Its optional weighted-logic prompt helps an agent scope one probability analysis and then returns control to the normal workflow.
|
|
92
99
|
|
|
93
|
-
Connecting and listing tools does not scan mod source. Compact tool schemas and linked resources keep large diagnostics, renders, and diffs out of the agent's working context until needed. Event, technology, and probability tools analyze source without editing it; only `hoi4.*_rewrite` calls edit mod source.
|
|
100
|
+
Connecting and listing tools does not scan mod source. Compact tool schemas and linked resources keep large diagnostics, renders, and diffs out of the agent's working context until needed. Event, impact, decision, technology, and probability tools analyze source without editing it; only `hoi4.*_rewrite` calls edit mod source.
|
|
94
101
|
|
|
95
102
|
## Create or clean content
|
|
96
103
|
|
|
@@ -100,6 +107,7 @@ Ask your agent in normal task language. A typical workflow is inspect, render, r
|
|
|
100
107
|
- Scripted GUIs: "Create a scripted GUI for this mechanic," or "Render every value-driven version of this window and fix hidden controls, off-center button text, background alignment, clipping, and click-region conflicts." See [Scripted GUIs](docs/gui.md).
|
|
101
108
|
- Maps: "Render the whole map and find this state by name," "Create a state from these provinces," "Create a province inside this exact rectangle," or "Swap these state IDs and update connected references." See [Maps](docs/map.md).
|
|
102
109
|
- Event chains: "Trace every route from this event and explain where its flags and variables change," or "Compare the workspace event graph with its previous revision and render the affected routes." See [Event chains](docs/events.md).
|
|
110
|
+
- Cross-system impact and decisions: "Find every direct and transitive consumer of this idea," or "Compare who can take this decision, what they pay, and its mission end paths for these actor and target scenarios." See [Cross-system impact and decisions](docs/analysis.md).
|
|
103
111
|
- Technology trees: "Explain everything this technology requires and unlocks," or "Compare this technology patch and render every affected folder and doctrine branch." See [Technology trees](docs/technology.md).
|
|
104
112
|
- AI and MTTH: "Compare these focus weights across peace, defensive-war, and low-stability scenarios," or "Show when this MTTH event becomes likely and which unknown inputs control the result." See [AI and MTTH analysis](docs/probability.md).
|
|
105
113
|
|
package/dist/bin/stdio.js
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { canonicalJson } from '../hoi4_agent_tools/core/canonical.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { FinalProtocolTransport } from '../hoi4_agent_tools/mcp/transports/protocol-gate.js';
|
|
3
|
+
import { StdioFrameLimitError, StdioInvalidMessageError, StdioInvalidUtf8Error, } from '../hoi4_agent_tools/mcp/transports/bounded-stdio.js';
|
|
4
|
+
import { serveNegotiatedStdio } from '../hoi4_agent_tools/mcp/transports/negotiated-stdio.js';
|
|
6
5
|
import { createEngine } from '../hoi4_agent_tools/runtime.js';
|
|
7
6
|
async function main() {
|
|
8
7
|
const engine = await createEngine();
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
registerChaosxTools(server, engine, context);
|
|
14
|
-
}
|
|
15
|
-
server.server.onerror = (error) => {
|
|
8
|
+
const chaosxTools = process.env.HOI4_AGENT_TOOLS_CHAOSX === '1'
|
|
9
|
+
? await import('../hoi4_agent_tools/mcp/tools/chaosx.js')
|
|
10
|
+
: undefined;
|
|
11
|
+
const onerror = (error) => {
|
|
16
12
|
const fatalInputError = error instanceof StdioFrameLimitError || error instanceof StdioInvalidUtf8Error;
|
|
17
13
|
const rejectedMessage = error instanceof StdioInvalidMessageError;
|
|
18
14
|
if (fatalInputError)
|
|
@@ -24,8 +20,23 @@ async function main() {
|
|
|
24
20
|
message: error.message,
|
|
25
21
|
})}\n`);
|
|
26
22
|
};
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
await serveNegotiatedStdio(engine, {
|
|
24
|
+
onerror,
|
|
25
|
+
onFailure: (error) => {
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
process.stderr.write(`${canonicalJson({
|
|
28
|
+
level: 'error',
|
|
29
|
+
event: 'transport_startup_failed',
|
|
30
|
+
message: error instanceof Error ? error.message : String(error),
|
|
31
|
+
})}\n`);
|
|
32
|
+
},
|
|
33
|
+
...(chaosxTools === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: {
|
|
36
|
+
createModernPrivateTools: chaosxTools.createChaosxToolOperations,
|
|
37
|
+
registerLegacy: (server) => chaosxTools.registerChaosxTools(server, engine, {}),
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
29
40
|
}
|
|
30
41
|
main().catch((error) => {
|
|
31
42
|
process.stderr.write(`${canonicalJson({
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { z } from 'zod/v4';
|
|
3
|
+
import { canonicalJson, hashCanonical } from './canonical.js';
|
|
4
|
+
import { ServiceError } from './result.js';
|
|
5
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
6
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
7
|
+
const envelopeSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
version: z.literal(1),
|
|
10
|
+
key: digest,
|
|
11
|
+
sourceRevision: digest,
|
|
12
|
+
state: z.json(),
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
const active = new AsyncLocalStorage();
|
|
16
|
+
/** Internal typed execution context; no transport or caller-supplied executable hooks. */
|
|
17
|
+
export function withAnalysisCheckpoints(checkpoints, run) {
|
|
18
|
+
return active.run(checkpoints, run);
|
|
19
|
+
}
|
|
20
|
+
export function currentAnalysisCheckpoints() {
|
|
21
|
+
return active.getStore();
|
|
22
|
+
}
|
|
23
|
+
/** The authenticated job record commits to the exact immutable checkpoint bytes. */
|
|
24
|
+
export function jobAnalysisCheckpoints(context) {
|
|
25
|
+
let checkpoint = context.checkpoint;
|
|
26
|
+
const workspace = context.engine.resolver.get(context.workspaceId, context.principal);
|
|
27
|
+
return {
|
|
28
|
+
async load(key, sourceRevision, schema) {
|
|
29
|
+
if (checkpoint?.cursor !== `analysis:${key}` || checkpoint.sourceRevision !== sourceRevision)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (checkpoint.resourceUri === undefined || checkpoint.stateHash === undefined)
|
|
32
|
+
throw new ServiceError('ANALYSIS_CHECKPOINT_INVALID', 'Analysis checkpoint has no authenticated state');
|
|
33
|
+
const resource = await context.engine.artifacts.readLogical(workspace, checkpoint.resourceUri, {
|
|
34
|
+
mimeType: 'application/json',
|
|
35
|
+
maxBytes: 268_435_456,
|
|
36
|
+
maxChunks: 256,
|
|
37
|
+
}, context.signal);
|
|
38
|
+
const envelope = envelopeSchema.parse(JSON.parse(resource.bytes.toString('utf8')));
|
|
39
|
+
if (envelope.key !== key ||
|
|
40
|
+
envelope.sourceRevision !== sourceRevision ||
|
|
41
|
+
hashCanonical(envelope) !== checkpoint.stateHash)
|
|
42
|
+
throw new ServiceError('ANALYSIS_CHECKPOINT_INVALID', 'Analysis checkpoint identity or content does not match its job');
|
|
43
|
+
return schema.parse(envelope.state);
|
|
44
|
+
},
|
|
45
|
+
async save(key, sourceRevision, state, completed, total) {
|
|
46
|
+
context.signal.throwIfAborted();
|
|
47
|
+
const envelope = envelopeSchema.parse({ version: 1, key, sourceRevision, state });
|
|
48
|
+
await context.engine.artifacts.withAtomicChunkedWrites(workspace, [
|
|
49
|
+
{
|
|
50
|
+
name: 'analysis-checkpoint.json',
|
|
51
|
+
mimeType: 'application/json',
|
|
52
|
+
content: canonicalJson(envelope),
|
|
53
|
+
provenance: {
|
|
54
|
+
kind: 'analysis-checkpoint',
|
|
55
|
+
toolVersion: PACKAGE_VERSION,
|
|
56
|
+
schemaVersion: '1',
|
|
57
|
+
sourceHashes: {},
|
|
58
|
+
metadata: { key, sourceRevision },
|
|
59
|
+
},
|
|
60
|
+
description: 'Revision-bound internal dependency-expansion checkpoint',
|
|
61
|
+
},
|
|
62
|
+
], async ([resource], physical) => {
|
|
63
|
+
const next = {
|
|
64
|
+
sourceRevision,
|
|
65
|
+
resourceUri: resource.uri,
|
|
66
|
+
cursor: `analysis:${key}`,
|
|
67
|
+
stateHash: hashCanonical(envelope),
|
|
68
|
+
resources: physical.map(({ uri }) => uri),
|
|
69
|
+
};
|
|
70
|
+
// Publish the authenticated pin before releasing artifact admission. A competing
|
|
71
|
+
// writer therefore sees either the old complete checkpoint or the new one.
|
|
72
|
+
await context.saveCheckpoint(next);
|
|
73
|
+
checkpoint = next;
|
|
74
|
+
}, context.signal);
|
|
75
|
+
await context.progress({
|
|
76
|
+
completed,
|
|
77
|
+
total,
|
|
78
|
+
message: 'Expanding source-bound helper dependencies',
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { boundedSourceHashEvidence } from './artifacts.js';
|
|
2
|
+
/** Bind a linked analysis report to every source in both compared revisions. */
|
|
3
|
+
export function analysisSourceEvidence(before, after, additionalSources = {}) {
|
|
4
|
+
return boundedSourceHashEvidence(Object.fromEntries([
|
|
5
|
+
...before.files.map((file) => [`before:${file.displayPath}`, file.sha256]),
|
|
6
|
+
...(after?.files.map((file) => [`after:${file.displayPath}`, file.sha256]) ?? []),
|
|
7
|
+
...Object.entries(additionalSources),
|
|
8
|
+
]));
|
|
9
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { DecisionAnalyzer } from '../decision/service.js';
|
|
3
|
+
import { ImpactAnalyzer } from '../impact/service.js';
|
|
4
|
+
import { decisionInspectRequestSchema, impactInspectRequestSchema } from '../schemas/analysis.js';
|
|
5
|
+
import { errorResult, toolResult } from './operation-result.js';
|
|
6
|
+
import { ServiceError } from './result.js';
|
|
7
|
+
const wireResult = z.record(z.string(), z.json());
|
|
8
|
+
/** Registers the two read-only analysis services for durable ordinary and native tasks. */
|
|
9
|
+
export function registerAnalysisJobs(operations, engine) {
|
|
10
|
+
const impact = new ImpactAnalyzer(engine);
|
|
11
|
+
const decision = new DecisionAnalyzer(engine);
|
|
12
|
+
const register = (name, schema, execute, progressMessage) => {
|
|
13
|
+
operations.registerRead(name, schema, async (input, context) => {
|
|
14
|
+
try {
|
|
15
|
+
if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
|
|
16
|
+
throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
|
|
17
|
+
await context.progress({ completed: 0, total: 2, message: progressMessage });
|
|
18
|
+
const result = await execute({
|
|
19
|
+
...input,
|
|
20
|
+
workspaceId: context.workspaceId,
|
|
21
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
22
|
+
signal: context.signal,
|
|
23
|
+
});
|
|
24
|
+
await context.progress({ completed: 2, total: 2, message: 'Analysis complete' });
|
|
25
|
+
const wire = wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
|
|
26
|
+
await context.stageResult(wire, { sourceRevision: result.data.sourceRevision });
|
|
27
|
+
return wire;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (context.signal.aborted)
|
|
31
|
+
throw error;
|
|
32
|
+
return wireResult.parse(JSON.parse(JSON.stringify(errorResult(error, context.workspaceId))));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
register('hoi4.impact_inspect', impactInspectRequestSchema, (input) => impact.inspect(input), 'Tracing cross-system impact');
|
|
37
|
+
register('hoi4.decision_inspect', decisionInspectRequestSchema, (input) => decision.inspect(input), 'Analyzing decisions and missions');
|
|
38
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { canonicalJson } from './canonical.js';
|
|
2
|
+
import { ServiceError } from './result.js';
|
|
3
|
+
const artifactResourceLimit = 1_048_576;
|
|
4
|
+
const artifactChunkMetadataKey = 'io.github.klimpaskov/hoi4-agent-tools.artifact-byte-range';
|
|
5
|
+
const fatalUtf8Decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
6
|
+
export const artifactResourceDefinition = {
|
|
7
|
+
name: 'artifact',
|
|
8
|
+
uriTemplate: 'hoi4-agent://workspace/{workspaceId}/artifact/{sha256}/{provenanceHash}/{name}',
|
|
9
|
+
title: 'HOI4 review artifact',
|
|
10
|
+
description: 'Complete plans, diagnostics, layouts, previews, event graphs, routes, state and scope reports, comparisons, and diffs. Reads accept zero-based byte offset and positive byte length query selectors; each response reports its exact byte range and continuation URI in _meta.',
|
|
11
|
+
_meta: {
|
|
12
|
+
[artifactChunkMetadataKey]: {
|
|
13
|
+
version: 1,
|
|
14
|
+
unit: 'byte',
|
|
15
|
+
maxChunkSize: artifactResourceLimit,
|
|
16
|
+
selectors: {
|
|
17
|
+
offset: { type: 'integer', minimum: 0, default: 0 },
|
|
18
|
+
length: {
|
|
19
|
+
type: 'integer',
|
|
20
|
+
minimum: 1,
|
|
21
|
+
maximum: artifactResourceLimit,
|
|
22
|
+
default: artifactResourceLimit,
|
|
23
|
+
},
|
|
24
|
+
metadata: { type: 'string', enum: ['manifest'], optional: true },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
function assertResourceSelectors(uri, allowed) {
|
|
30
|
+
if (uri.hash !== '' || uri.username !== '' || uri.password !== '' || uri.port !== '') {
|
|
31
|
+
throw new ServiceError('RESOURCE_URI_INVALID', 'Resource URI is not canonical');
|
|
32
|
+
}
|
|
33
|
+
const allowedNames = new Set(allowed);
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const name of uri.searchParams.keys()) {
|
|
36
|
+
if (!allowedNames.has(name)) {
|
|
37
|
+
throw new ServiceError('RESOURCE_SELECTOR_INVALID', 'Resource selector is not supported');
|
|
38
|
+
}
|
|
39
|
+
if (seen.has(name)) {
|
|
40
|
+
throw new ServiceError('RESOURCE_SELECTOR_INVALID', 'Resource selectors may not be repeated');
|
|
41
|
+
}
|
|
42
|
+
seen.add(name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function contentArtifactUri(uri) {
|
|
46
|
+
const contentUri = new URL(uri);
|
|
47
|
+
contentUri.search = '';
|
|
48
|
+
contentUri.hash = '';
|
|
49
|
+
return contentUri.href;
|
|
50
|
+
}
|
|
51
|
+
function exactUtf8Text(bytes) {
|
|
52
|
+
try {
|
|
53
|
+
const text = fatalUtf8Decoder.decode(bytes);
|
|
54
|
+
return Buffer.from(text, 'utf8').equals(bytes) ? text : undefined;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function artifactRangeParameter(uri, name, fallback, minimum, maximum = Number.MAX_SAFE_INTEGER) {
|
|
61
|
+
const raw = uri.searchParams.get(name);
|
|
62
|
+
if (raw === null)
|
|
63
|
+
return fallback;
|
|
64
|
+
if (!/^\d+$/u.test(raw)) {
|
|
65
|
+
throw new ServiceError('ARTIFACT_RANGE_INVALID', 'Artifact byte range is invalid');
|
|
66
|
+
}
|
|
67
|
+
const value = Number(raw);
|
|
68
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
69
|
+
throw new ServiceError('ARTIFACT_RANGE_INVALID', 'Artifact byte range is invalid');
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function artifactChunkMetadata(uri, metadata, totalSize, offset, returnedLength, requestedLength) {
|
|
74
|
+
const endExclusive = offset + returnedLength;
|
|
75
|
+
let continuationUri = null;
|
|
76
|
+
if (endExclusive < totalSize) {
|
|
77
|
+
const continuation = new URL(contentArtifactUri(uri));
|
|
78
|
+
continuation.searchParams.set('offset', String(endExclusive));
|
|
79
|
+
continuation.searchParams.set('length', String(requestedLength));
|
|
80
|
+
if (metadata !== null)
|
|
81
|
+
continuation.searchParams.set('metadata', metadata);
|
|
82
|
+
continuationUri = continuation.href;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
[artifactChunkMetadataKey]: {
|
|
86
|
+
version: 1,
|
|
87
|
+
unit: 'byte',
|
|
88
|
+
totalSize,
|
|
89
|
+
returnedRange: { offset, length: returnedLength, endExclusive },
|
|
90
|
+
complete: offset === 0 && returnedLength === totalSize,
|
|
91
|
+
continuationUri,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Shared bounded resource reader. URI ownership never supplies a principal or root grant. */
|
|
96
|
+
export async function readArtifactResource(engine, uriInput, context, signal) {
|
|
97
|
+
signal.throwIfAborted();
|
|
98
|
+
let uri;
|
|
99
|
+
let workspaceId;
|
|
100
|
+
try {
|
|
101
|
+
if (uriInput.length > 8192)
|
|
102
|
+
throw new Error('URI exceeds the resource limit');
|
|
103
|
+
uri = new URL(uriInput);
|
|
104
|
+
const segments = uri.pathname.split('/');
|
|
105
|
+
if (uri.protocol !== 'hoi4-agent:' ||
|
|
106
|
+
uri.hostname !== 'workspace' ||
|
|
107
|
+
segments.length !== 6 ||
|
|
108
|
+
segments[0] !== '' ||
|
|
109
|
+
segments[2] !== 'artifact' ||
|
|
110
|
+
segments.some((segment, index) => index !== 0 && segment === ''))
|
|
111
|
+
throw new Error('Invalid resource path');
|
|
112
|
+
workspaceId = decodeURIComponent(segments[1]);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
throw new ServiceError('RESOURCE_URI_INVALID', 'Resource URI is invalid');
|
|
116
|
+
}
|
|
117
|
+
signal.throwIfAborted();
|
|
118
|
+
assertResourceSelectors(uri, ['offset', 'length', 'metadata']);
|
|
119
|
+
const workspace = engine.resolver.get(workspaceId, context.principal);
|
|
120
|
+
const offset = artifactRangeParameter(uri, 'offset', 0, 0);
|
|
121
|
+
const length = artifactRangeParameter(uri, 'length', artifactResourceLimit, 1, artifactResourceLimit);
|
|
122
|
+
const metadata = uri.searchParams.get('metadata');
|
|
123
|
+
if (metadata !== null && metadata !== 'manifest') {
|
|
124
|
+
throw new ServiceError('ARTIFACT_METADATA_INVALID', 'Artifact metadata selector is invalid');
|
|
125
|
+
}
|
|
126
|
+
if (metadata === 'manifest') {
|
|
127
|
+
const described = await engine.artifacts.describe(workspace, contentArtifactUri(uri), signal);
|
|
128
|
+
const { path: artifactPath, ...publicManifest } = described;
|
|
129
|
+
void artifactPath;
|
|
130
|
+
const manifestBytes = Buffer.from(`${canonicalJson(publicManifest)}\n`, 'utf8');
|
|
131
|
+
const start = Math.min(offset, manifestBytes.length);
|
|
132
|
+
const selected = manifestBytes.subarray(start, Math.min(manifestBytes.length, start + length));
|
|
133
|
+
const complete = start === 0 && selected.length === manifestBytes.length;
|
|
134
|
+
const _meta = artifactChunkMetadata(uri, metadata, manifestBytes.length, start, selected.length, length);
|
|
135
|
+
return {
|
|
136
|
+
contents: [
|
|
137
|
+
complete
|
|
138
|
+
? {
|
|
139
|
+
uri: uri.href,
|
|
140
|
+
mimeType: 'application/json',
|
|
141
|
+
text: selected.toString('utf8'),
|
|
142
|
+
_meta,
|
|
143
|
+
}
|
|
144
|
+
: {
|
|
145
|
+
uri: uri.href,
|
|
146
|
+
mimeType: 'application/json',
|
|
147
|
+
blob: selected.toString('base64'),
|
|
148
|
+
_meta,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const artifact = await engine.artifacts.read(workspace, contentArtifactUri(uri), { offset, length }, signal);
|
|
154
|
+
const textual = artifact.mimeType.startsWith('text/') ||
|
|
155
|
+
artifact.mimeType === 'application/json' ||
|
|
156
|
+
artifact.mimeType === 'image/svg+xml';
|
|
157
|
+
const start = Math.min(offset, artifact.totalSize);
|
|
158
|
+
const complete = start === 0 && artifact.bytes.length === artifact.totalSize;
|
|
159
|
+
const text = textual && complete ? exactUtf8Text(artifact.bytes) : undefined;
|
|
160
|
+
const _meta = artifactChunkMetadata(uri, metadata, artifact.totalSize, start, artifact.bytes.length, length);
|
|
161
|
+
return {
|
|
162
|
+
contents: [
|
|
163
|
+
text !== undefined
|
|
164
|
+
? { uri: uri.href, mimeType: artifact.mimeType, text, _meta }
|
|
165
|
+
: {
|
|
166
|
+
uri: uri.href,
|
|
167
|
+
mimeType: artifact.mimeType,
|
|
168
|
+
blob: artifact.bytes.toString('base64'),
|
|
169
|
+
_meta,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -707,17 +707,27 @@ export class ArtifactStore {
|
|
|
707
707
|
maxBytes;
|
|
708
708
|
maxEntries;
|
|
709
709
|
maxSingleBytes;
|
|
710
|
+
retention;
|
|
710
711
|
#verifiedContent = new Map();
|
|
711
|
-
constructor(maxBytes = 536_870_912, maxEntries = 5_000, maxSingleBytes = 134_217_728) {
|
|
712
|
+
constructor(maxBytes = 536_870_912, maxEntries = 5_000, maxSingleBytes = 134_217_728, retention) {
|
|
712
713
|
this.maxBytes = maxBytes;
|
|
713
714
|
this.maxEntries = maxEntries;
|
|
714
715
|
this.maxSingleBytes = maxSingleBytes;
|
|
716
|
+
this.retention = retention;
|
|
715
717
|
}
|
|
716
718
|
async pruneForAdmission(workspace, usage, additionalBytes, additionalEntries, protectedManifests, protectedTargets, signal) {
|
|
717
719
|
if (usage.entries + additionalEntries <= this.maxEntries &&
|
|
718
720
|
usage.bytes <= this.maxBytes - additionalBytes) {
|
|
719
721
|
return usage;
|
|
720
722
|
}
|
|
723
|
+
// Read durable pins while holding the same cross-process publication lock used by
|
|
724
|
+
// checkpoint commits. A committed frontier cannot disappear between save and resume.
|
|
725
|
+
for (const uri of (await this.retention?.(workspace, signal)) ?? []) {
|
|
726
|
+
signal?.throwIfAborted();
|
|
727
|
+
const { sha256, provenanceHash, name } = this.parseUri(workspace, uri);
|
|
728
|
+
protectedTargets.add(await containedGeneratedPath(workspace.artifactRoot, sha256.slice(0, 2), sha256, name));
|
|
729
|
+
protectedManifests.add(await containedGeneratedPath(workspace.artifactRoot, sha256.slice(0, 2), sha256, `${name}.${provenanceHash}.manifest.json`));
|
|
730
|
+
}
|
|
721
731
|
// Reclaim to a low-water mark so a busy agent workflow does not rescan and evict one artifact
|
|
722
732
|
// for every subsequent tool call. The incoming batch itself remains protected and atomic.
|
|
723
733
|
const targetBytes = Math.max(0, Math.min(this.maxBytes - additionalBytes, Math.floor(this.maxBytes * 0.75)));
|
|
@@ -1222,7 +1232,7 @@ export class ArtifactStore {
|
|
|
1222
1232
|
if (physicalWrites.length !== admission.projectedEntries) {
|
|
1223
1233
|
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Artifact batch entry projection did not match prepared output');
|
|
1224
1234
|
}
|
|
1225
|
-
return this.withAtomicWrites(workspace, physicalWrites, (stored) => commit(logicalIndexes.map((index) => stored[index])), signal);
|
|
1235
|
+
return this.withAtomicWrites(workspace, physicalWrites, (stored) => commit(logicalIndexes.map((index) => stored[index]), stored), signal);
|
|
1226
1236
|
}
|
|
1227
1237
|
plannedArtifactLink(workspace, write, bytes, sha256) {
|
|
1228
1238
|
const manifestWithoutHash = {
|
|
@@ -1458,6 +1468,78 @@ export class ArtifactStore {
|
|
|
1458
1468
|
await refreshArtifactManifest(manifestPath);
|
|
1459
1469
|
return { bytes, mimeType: manifest.mimeType, name, totalSize };
|
|
1460
1470
|
}
|
|
1471
|
+
/** Read either one stored object or an authenticated chunk index as exact logical bytes. */
|
|
1472
|
+
async readLogical(workspace, uri, options, signal) {
|
|
1473
|
+
const initial = await this.read(workspace, uri, undefined, signal);
|
|
1474
|
+
if (initial.mimeType !== options.mimeType)
|
|
1475
|
+
throw new ServiceError('ARTIFACT_LOGICAL_TYPE_MISMATCH', `Logical artifact must use ${options.mimeType}`);
|
|
1476
|
+
let parsed;
|
|
1477
|
+
try {
|
|
1478
|
+
parsed = JSON.parse(initial.bytes.toString('utf8'));
|
|
1479
|
+
}
|
|
1480
|
+
catch {
|
|
1481
|
+
if (initial.totalSize > options.maxBytes)
|
|
1482
|
+
throw new ServiceError('ARTIFACT_LOGICAL_LIMIT', 'Logical artifact exceeds its fixed byte ceiling');
|
|
1483
|
+
return initial;
|
|
1484
|
+
}
|
|
1485
|
+
if (parsed === null ||
|
|
1486
|
+
typeof parsed !== 'object' ||
|
|
1487
|
+
Array.isArray(parsed) ||
|
|
1488
|
+
parsed.type !== 'hoi4-agent.chunked-artifact') {
|
|
1489
|
+
if (initial.totalSize > options.maxBytes)
|
|
1490
|
+
throw new ServiceError('ARTIFACT_LOGICAL_LIMIT', 'Logical artifact exceeds its fixed byte ceiling');
|
|
1491
|
+
return initial;
|
|
1492
|
+
}
|
|
1493
|
+
const index = parsed;
|
|
1494
|
+
if (index.schemaVersion !== 1 ||
|
|
1495
|
+
index.original === undefined ||
|
|
1496
|
+
!Array.isArray(index.chunks) ||
|
|
1497
|
+
index.chunks.length < 1 ||
|
|
1498
|
+
index.chunks.length > options.maxChunks ||
|
|
1499
|
+
index.original.mimeType !== options.mimeType ||
|
|
1500
|
+
!Number.isSafeInteger(index.original.size) ||
|
|
1501
|
+
index.original.size < 0 ||
|
|
1502
|
+
index.original.size > options.maxBytes ||
|
|
1503
|
+
!/^[a-f0-9]{64}$/u.test(index.original.sha256))
|
|
1504
|
+
throw new ServiceError('ARTIFACT_LOGICAL_INVALID', 'Chunked logical artifact index is malformed or exceeds its fixed limits');
|
|
1505
|
+
const buffers = [];
|
|
1506
|
+
const chunkUris = new Set();
|
|
1507
|
+
let offset = 0;
|
|
1508
|
+
for (const [chunkIndex, candidate] of index.chunks.entries()) {
|
|
1509
|
+
signal?.throwIfAborted();
|
|
1510
|
+
if (candidate.index !== chunkIndex ||
|
|
1511
|
+
candidate.offset !== offset ||
|
|
1512
|
+
!Number.isSafeInteger(candidate.length) ||
|
|
1513
|
+
candidate.length <= 0 ||
|
|
1514
|
+
candidate.size !== candidate.length ||
|
|
1515
|
+
candidate.mimeType !== 'application/octet-stream' ||
|
|
1516
|
+
typeof candidate.uri !== 'string' ||
|
|
1517
|
+
chunkUris.has(candidate.uri) ||
|
|
1518
|
+
!/^[a-f0-9]{64}$/u.test(candidate.sha256) ||
|
|
1519
|
+
offset + candidate.length > index.original.size)
|
|
1520
|
+
throw new ServiceError('ARTIFACT_LOGICAL_INVALID', 'Chunked logical artifact entry is malformed');
|
|
1521
|
+
chunkUris.add(candidate.uri);
|
|
1522
|
+
const chunk = await this.read(workspace, candidate.uri, undefined, signal);
|
|
1523
|
+
if (chunk.mimeType !== 'application/octet-stream' ||
|
|
1524
|
+
chunk.totalSize !== candidate.length ||
|
|
1525
|
+
chunk.bytes.length !== candidate.length ||
|
|
1526
|
+
sha256Bytes(chunk.bytes) !== candidate.sha256)
|
|
1527
|
+
throw new ServiceError('ARTIFACT_LOGICAL_INVALID', 'Chunked logical artifact content does not match its authenticated index');
|
|
1528
|
+
buffers.push(chunk.bytes);
|
|
1529
|
+
offset += chunk.bytes.length;
|
|
1530
|
+
}
|
|
1531
|
+
const bytes = Buffer.concat(buffers, index.original.size);
|
|
1532
|
+
if (offset !== index.original.size ||
|
|
1533
|
+
bytes.length !== index.original.size ||
|
|
1534
|
+
sha256Bytes(bytes) !== index.original.sha256)
|
|
1535
|
+
throw new ServiceError('ARTIFACT_LOGICAL_INVALID', 'Chunked logical artifact does not reconstruct its declared content');
|
|
1536
|
+
return {
|
|
1537
|
+
bytes,
|
|
1538
|
+
mimeType: index.original.mimeType,
|
|
1539
|
+
name: index.original.name,
|
|
1540
|
+
totalSize: index.original.size,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1461
1543
|
uri(workspaceId, sha256, provenanceHash, name) {
|
|
1462
1544
|
return `hoi4-agent://workspace/${encodeURIComponent(workspaceId)}/artifact/${sha256}/${provenanceHash}/${encodeURIComponent(name)}`;
|
|
1463
1545
|
}
|
|
@@ -344,6 +344,20 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
344
344
|
state: compare(leftNumber, rightNumber, assignment.operator.text) ? 'true' : 'false',
|
|
345
345
|
unresolved: [],
|
|
346
346
|
};
|
|
347
|
+
if (/^(?:ROOT|THIS|PREV|FROM)$/iu.test(right)) {
|
|
348
|
+
const comparisonScope = resolveScopeContext(scenario, right, scopeContext);
|
|
349
|
+
if (comparisonScope === undefined ||
|
|
350
|
+
(comparisonScope.expression === 'ROOT' && scenario.actor === undefined))
|
|
351
|
+
return unresolved(assignment, candidate, `Scenario does not bind scope ${right}`);
|
|
352
|
+
if (typeof declared !== 'string')
|
|
353
|
+
return unresolved(assignment, candidate, `Trigger ${key} cannot compare its declared value with scope ${right}`);
|
|
354
|
+
return {
|
|
355
|
+
state: compare(declared, scopeIdentity(comparisonScope), assignment.operator.text)
|
|
356
|
+
? 'true'
|
|
357
|
+
: 'false',
|
|
358
|
+
unresolved: [],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
347
361
|
if (typeof declared === 'string')
|
|
348
362
|
return {
|
|
349
363
|
state: compare(declared, right, assignment.operator.text) ? 'true' : 'false',
|
|
@@ -397,3 +411,5 @@ export function evaluateTriggerBlock(block, scenario, candidate, definitions, he
|
|
|
397
411
|
trace: children.flatMap(({ trace }) => trace ?? []),
|
|
398
412
|
};
|
|
399
413
|
}
|
|
414
|
+
/** Resolve a declared numeric expression with the same constants and scope rules as triggers. */
|
|
415
|
+
export { numberValue as resolveConditionNumber };
|