rea-agents 2.1.0 → 2.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.
Files changed (48) hide show
  1. package/README.md +29 -10
  2. package/dist/application/AndroidApplicationService.js +17 -0
  3. package/dist/application/AppleApplicationService.js +17 -0
  4. package/dist/application/ClientRegistrationStatus.js +11 -4
  5. package/dist/application/InventoryProjectionEvidence.js +41 -0
  6. package/dist/application/InvestigationProviders.js +12 -0
  7. package/dist/application/ProcessPairedExperiment.js +72 -0
  8. package/dist/application/Setup.js +3 -3
  9. package/dist/application/SetupClientConfiguration.js +10 -6
  10. package/dist/application/SetupPlan.js +3 -2
  11. package/dist/application/SetupSkill.js +44 -40
  12. package/dist/application/Uninstall.js +7 -1
  13. package/dist/application/Upgrade.js +16 -1
  14. package/dist/cli/setupCommands.js +8 -3
  15. package/dist/cliSetup.js +29 -12
  16. package/dist/contracts/electronToolContracts.js +51 -3
  17. package/dist/contracts/managedToolContracts.js +7 -2
  18. package/dist/contracts/sessionStatusContract.js +26 -0
  19. package/dist/contracts/toolContractTypes.js +12 -1
  20. package/dist/contracts/toolContracts.js +3 -10
  21. package/dist/contracts/toolOutputSchemaGroups.js +51 -9
  22. package/dist/contracts/toolOutputSchemaPrimitives.js +33 -32
  23. package/dist/domain/androidApplication.js +206 -0
  24. package/dist/domain/appleApplication.js +221 -0
  25. package/dist/domain/boundedCartesianProjection.js +15 -0
  26. package/dist/domain/processComparison.js +19 -0
  27. package/dist/domain/processPairedExperiment.js +128 -0
  28. package/dist/domain/replayMachine.js +316 -0
  29. package/dist/domain/replayMachineRuntime.js +215 -0
  30. package/dist/domain/runtimeIdentification.js +210 -0
  31. package/dist/evaluation/CodexAgentEval.js +132 -0
  32. package/dist/generatedPackageMetadata.js +2 -2
  33. package/dist/identity.js +2 -1
  34. package/dist/server/createServer.js +5 -2
  35. package/dist/server/javascriptApplicationResult.js +85 -0
  36. package/dist/server/registerElectronTools.js +19 -5
  37. package/dist/server/registerEvidenceResources.js +2 -0
  38. package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
  39. package/dist/server/registerManagedTools.js +18 -1
  40. package/dist/server/registerSessionStatusTool.js +95 -25
  41. package/dist/server/toolResult.js +10 -4
  42. package/package.json +2 -1
  43. package/skills/reverse-engineer-anything/SKILL.md +68 -227
  44. package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
  45. package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
  46. package/skills/reverse-engineer-anything/references/javascript-applications.md +25 -0
  47. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
  48. package/skills/reverse-engineer-anything/references/runtime-observation.md +24 -0
package/dist/cliSetup.js CHANGED
@@ -9,7 +9,7 @@ const promptStreams = {
9
9
  output: process.stderr,
10
10
  withGuide: true,
11
11
  };
12
- const agentAccessCapabilityId = "agent_access";
12
+ const agentIntegrationCapabilityId = "agent_integration";
13
13
  /** Run the inline setup picker, exact preflight, and default-No consent. */
14
14
  export const confirmInteractiveSetup = async (actions, accessible) => {
15
15
  intro("REA setup", promptStreams);
@@ -96,29 +96,46 @@ const selectSetupActions = async (actions, accessible) => {
96
96
  if (selectedCapabilities === undefined)
97
97
  return undefined;
98
98
  const selectedCapabilityIds = new Set(selectedCapabilities);
99
+ const bundledSkillAction = componentActions.find(({ kind }) => kind === "install_skill");
99
100
  const selectedActionIds = componentActions
100
- .filter(({ id }) => selectedCapabilityIds.has(id))
101
+ .filter(({ id }) => id !== bundledSkillAction?.id && selectedCapabilityIds.has(id))
101
102
  .map(({ id }) => id);
102
- if (!selectedCapabilityIds.has(agentAccessCapabilityId))
103
+ if (!selectedCapabilityIds.has(agentIntegrationCapabilityId))
103
104
  return selectedActionIds;
105
+ if (clientActions.length === 0)
106
+ return [
107
+ ...(bundledSkillAction === undefined ? [] : [bundledSkillAction.id]),
108
+ ...selectedActionIds,
109
+ ];
104
110
  const selectedClients = accessible
105
111
  ? await selectClientsAccessibly(clientActions)
106
112
  : await selectClients(clientActions);
107
- return selectedClients === undefined
108
- ? undefined
109
- : [...selectedClients, ...selectedActionIds];
113
+ if (selectedClients === undefined)
114
+ return undefined;
115
+ if (selectedClients.length === 0)
116
+ return selectedActionIds;
117
+ return [
118
+ ...selectedClients,
119
+ ...(bundledSkillAction === undefined ? [] : [bundledSkillAction.id]),
120
+ ...selectedActionIds,
121
+ ];
110
122
  };
111
123
  const setupCapabilities = (clientActions, componentActions) => [
112
- ...(clientActions.length === 0
124
+ ...(clientActions.length === 0 &&
125
+ !componentActions.some(({ kind }) => kind === "install_skill")
113
126
  ? []
114
127
  : [
115
128
  {
116
- id: agentAccessCapabilityId,
117
- label: "Coding-agent access (MCP)",
118
- hint: `${String(clientActions.length)} detected ${clientActions.length === 1 ? "agent" : "agents"}`,
129
+ id: agentIntegrationCapabilityId,
130
+ label: "Agent integration (MCP + guided workflow)",
131
+ hint: clientActions.length === 0
132
+ ? "MCP registrations are aligned; installs the matching bundled skill"
133
+ : `${String(clientActions.length)} detected ${clientActions.length === 1 ? "agent" : "agents"}; installs the bundled skill when needed`,
119
134
  },
120
135
  ]),
121
- ...componentActions.map((action) => ({
136
+ ...componentActions
137
+ .filter(({ kind }) => kind !== "install_skill")
138
+ .map((action) => ({
122
139
  id: action.id,
123
140
  label: `${action.label} (${actionModality(action)})`,
124
141
  hint: `${action.operation} · ${action.target}`,
@@ -223,7 +240,7 @@ const renderReadyCapabilities = (result, readyClients) => {
223
240
  writeLine("│ CLI: rea analyze /path/to/app");
224
241
  const firstClient = readyClients[0];
225
242
  if (firstClient !== undefined)
226
- writeLine(`│ Try in ${firstClient}: "Use REA to explain how a feature works in /path/to/app."`);
243
+ writeLine(`│ Try in ${firstClient}: "Explain how a feature works in /path/to/app and show the evidence."`);
227
244
  };
228
245
  const readyAgentClients = (result, changedClients) => {
229
246
  const alignedClients = (result.doctor.identity?.registrations ?? [])
@@ -1,12 +1,59 @@
1
+ import { z } from "zod";
1
2
  import { toolContractMetadata } from "./toolEffects.js";
2
3
  import { evidenceResultOf } from "./toolOutputSchemas.js";
3
4
  import { electronPageInspectionSchema, electronTargetListSchema, inspectElectronPageInputSchema, listElectronTargetsInputSchema, } from "../domain/electronObservation.js";
4
5
  import { analyzeJavaScriptApplicationInputSchema, javascriptApplicationAnalysisResultSchema, } from "../domain/javascriptApplicationAnalysis.js";
6
+ import { JAVASCRIPT_APPLICATION_NODE_KINDS } from "../domain/javascriptApplicationGraphSchemas.js";
7
+ import { applicationCoverageSchema } from "../domain/javascriptApplicationEvidenceSchemas.js";
5
8
  import { javascriptRuntimeReconciliationResultSchema, reconcileJavaScriptRuntimeInputSchema, } from "../domain/javascriptRuntimeReconciliationSchemas.js";
6
9
  import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "./javascriptRuntimeReconciliationExample.js";
7
10
  const listOutputSchema = evidenceResultOf(electronTargetListSchema);
8
11
  const inspectionOutputSchema = evidenceResultOf(electronPageInspectionSchema);
9
- const applicationOutputSchema = evidenceResultOf(javascriptApplicationAnalysisResultSchema);
12
+ /** MCP-only response selection; analysis identity does not depend on projection. */
13
+ export const analyzeJavaScriptApplicationToolInputSchema = analyzeJavaScriptApplicationInputSchema.extend({
14
+ detail: z
15
+ .enum(["summary", "full"])
16
+ .default("summary")
17
+ .describe("summary returns bounded findings and paged graph resource URIs; full returns the complete graph only when explicitly required"),
18
+ });
19
+ /** Bounded default projection for agent-facing JavaScript application analysis. */
20
+ export const javascriptApplicationAnalysisSummarySchema = javascriptApplicationAnalysisResultSchema.omit({ graph: true }).extend({
21
+ unknowns: z.array(z.string().min(1).max(4_096)).max(1_000),
22
+ graph: z.strictObject({
23
+ graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u),
24
+ node_count: z.number().int().min(0),
25
+ edge_count: z.number().int().min(0),
26
+ roots: z.strictObject({
27
+ items: z.array(z.string().regex(/^jag_node_[a-f0-9]{64}$/u)),
28
+ total: z.number().int().min(0),
29
+ truncated: z.boolean(),
30
+ }),
31
+ node_kinds: z.array(z.strictObject({
32
+ kind: z.enum(JAVASCRIPT_APPLICATION_NODE_KINDS),
33
+ count: z.number().int().min(1),
34
+ })),
35
+ top_findings: z.array(z.strictObject({
36
+ node_id: z.string().regex(/^jag_node_[a-f0-9]{64}$/u),
37
+ kind: z.enum(JAVASCRIPT_APPLICATION_NODE_KINDS),
38
+ label: z.string().nullable(),
39
+ authority: z.string(),
40
+ state: z.string(),
41
+ confidence: z.string(),
42
+ location: z.json(),
43
+ })),
44
+ coverage: applicationCoverageSchema,
45
+ limitations: z.array(z.string()),
46
+ pages: z.strictObject({
47
+ nodes: z.string().startsWith("rea://evidence/"),
48
+ edges: z.string().startsWith("rea://evidence/"),
49
+ page_limit: z.number().int().min(1).max(500),
50
+ }),
51
+ }),
52
+ });
53
+ const applicationOutputSchema = evidenceResultOf(z.union([
54
+ javascriptApplicationAnalysisSummarySchema,
55
+ javascriptApplicationAnalysisResultSchema,
56
+ ]));
10
57
  const reconciliationOutputSchema = evidenceResultOf(javascriptRuntimeReconciliationResultSchema);
11
58
  const endpoint = "http://127.0.0.1:9223";
12
59
  const root = "/Applications/Example.app/Contents/Resources";
@@ -66,9 +113,9 @@ export const ELECTRON_TOOL_CONTRACTS = [
66
113
  {
67
114
  name: "analyze_javascript_application",
68
115
  ...toolContractMetadata("analyze_javascript_application"),
69
- description: "Statically reconstruct one approved local ASAR or extracted JavaScript application as Evidence v2 and JavaScript Application Graph v1. Maps BrowserWindow preferences, preload and contextBridge APIs, literal and dynamic IPC, handler locations, sender-validation observations, utility processes, and requested native addon bindings without executing JavaScript; source-map contents require separate approval.",
116
+ description: "Statically reconstruct one approved local ASAR or extracted JavaScript application without executing it. The default summary returns architecture/security counts, top evidence-backed findings, unknowns, and paged graph resource URIs; request detail=full only when the complete graph must be returned immediately. Identical inputs are deterministic, so do not repeat the call or reread full Evidence without a specific missing detail.",
70
117
  kind: "electron-provider",
71
- inputSchema: analyzeJavaScriptApplicationInputSchema,
118
+ inputSchema: analyzeJavaScriptApplicationToolInputSchema,
72
119
  outputSchema: applicationOutputSchema,
73
120
  examples: [
74
121
  {
@@ -78,6 +125,7 @@ export const ELECTRON_TOOL_CONTRACTS = [
78
125
  format: "auto",
79
126
  approved: true,
80
127
  source_map_read_approved: false,
128
+ detail: "summary",
81
129
  },
82
130
  },
83
131
  ],
@@ -3,6 +3,11 @@ import { managedOutputSchemas } from "./toolOutputSchemas.js";
3
3
  import { toolContractMetadata } from "./toolEffects.js";
4
4
  /** Exact caller boundary for execution-free PE/CLI triage and identity. */
5
5
  export const managedArtifactInputSchema = z.object({
6
+ path: z
7
+ .string()
8
+ .min(1)
9
+ .optional()
10
+ .describe("Managed PE/CLI path to open before inspection; omit only when a managed target is already active"),
6
11
  reference_offset: z.number().int().min(0).default(0),
7
12
  reference_limit: z.number().int().min(1).max(500).default(100),
8
13
  resource_offset: z.number().int().min(0).default(0),
@@ -111,14 +116,14 @@ export const MANAGED_TOOL_CONTRACTS = [
111
116
  {
112
117
  name: "inspect_managed_artifact",
113
118
  ...toolContractMetadata("inspect_managed_artifact"),
114
- description: "Classify the active PE/CLI artifact and inventory exact assembly/module identity, target framework evidence, references, resources, and custom attributes without loading or executing target code. Returns bounded pages and explicit partial or malformed coverage.",
119
+ description: "Open and classify an explicit managed PE/CLI path, or inspect the active managed target, then inventory exact assembly/module identity, target framework evidence, references, resources, and custom attributes without loading or executing target code. Returns bounded pages and explicit partial or malformed coverage.",
115
120
  kind: "managed-provider",
116
121
  inputSchema: managedArtifactInputSchema,
117
122
  outputSchema,
118
123
  examples: [
119
124
  {
120
125
  title: "Example inspect managed artifact request",
121
- input: {},
126
+ input: { path: "/tmp/Example.dll" },
122
127
  },
123
128
  ],
124
129
  },
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { TOOL_KINDS } from "./toolContractTypes.js";
3
+ /** Caller observations and projection controls for binary_session. */
4
+ export const binarySessionInputSchema = z.object({
5
+ detail: z
6
+ .enum(["summary", "capabilities", "full"])
7
+ .default("summary")
8
+ .describe("summary returns routing state; capabilities returns one filtered tool-availability page; full returns complete diagnostics only when required"),
9
+ capability_family: z
10
+ .enum(TOOL_KINDS)
11
+ .optional()
12
+ .describe("Optional tool family for detail=capabilities"),
13
+ cursor: z
14
+ .number()
15
+ .int()
16
+ .min(0)
17
+ .default(0)
18
+ .describe("Zero-based capability cursor for detail=capabilities"),
19
+ limit: z.number().int().min(1).max(100).default(25),
20
+ expected_package_version: z.string().min(1).optional(),
21
+ expected_catalog_digest: z
22
+ .string()
23
+ .regex(/^[a-f0-9]{64}$/u)
24
+ .optional(),
25
+ expected_server_path: z.string().min(1).optional(),
26
+ });
@@ -1 +1,12 @@
1
- export {};
1
+ /** Canonical adapter families responsible for public MCP tools. */
2
+ export const TOOL_KINDS = [
3
+ "official-proxy",
4
+ "enhanced",
5
+ "native-provider",
6
+ "artifact-provider",
7
+ "managed-provider",
8
+ "browser-provider",
9
+ "electron-provider",
10
+ "application",
11
+ "session",
12
+ ];
@@ -21,6 +21,8 @@ import { ELECTRON_TOOL_CONTRACTS } from "./electronToolContracts.js";
21
21
  import { APPLICATION_TOOL_CONTRACTS } from "./applicationToolContracts.js";
22
22
  import { closeBinaryInputSchema, openBinaryInputSchema, } from "./sessionLifecycleInputs.js";
23
23
  import { toolContractMetadata } from "./toolEffects.js";
24
+ import { binarySessionInputSchema } from "./sessionStatusContract.js";
25
+ export { binarySessionInputSchema } from "./sessionStatusContract.js";
24
26
  const document = z.string().optional().describe("The document name");
25
27
  const address = z
26
28
  .string()
@@ -149,15 +151,6 @@ export const ENHANCED_TOOL_CONTRACTS = [
149
151
  enhanced("analyze_function", "Preferred bounded analysis for one procedure symbol or address. Returns identity, provider-specific pseudocode, optional assembly, comments, calls, typed-or-explicitly-unavailable references, referenced strings/names, and local CFG blocks with exact truncation metadata.", enhancedInputSchemas.analyze_function),
150
152
  enhanced("trace_feature", "Trace a bounded literal feature query through matching strings and procedures, xrefs, and truthful containing-procedure resolution. Returns the operation budget, truncation, and residual unknowns; unknown_registry_approved: true records them durably without inferring reference kinds.", enhancedInputSchemas.trace_feature),
151
153
  ];
152
- /** Caller observations used to compare a live server with expected identity. */
153
- export const binarySessionInputSchema = z.object({
154
- expected_package_version: z.string().min(1).optional(),
155
- expected_catalog_digest: z
156
- .string()
157
- .regex(/^[a-f0-9]{64}$/u)
158
- .optional(),
159
- expected_server_path: z.string().min(1).optional(),
160
- });
161
154
  /** Session-owned Evidence bundle export options. */
162
155
  export const exportEvidenceBundleInputSchema = z.strictObject({
163
156
  path: z.string().min(1).optional(),
@@ -193,7 +186,7 @@ export const verifyUnknownResolutionInputSchema = z.strictObject({
193
186
  export const SESSION_TOOL_CONTRACTS = [
194
187
  session("open_binary", "Open a local executable, application bundle, archive, JavaScript, source map, plist, or analysis database after validation. provider_id selects one deep provider or deterministic auto selection; the binding remains stable until close or an explicit switch, with no failure fallback. An optional snapshot v2 is imported atomically and must match the binary identity, concrete provider, and canonical analysis profile exactly.", openBinaryInputSchema),
195
188
  session("close_binary", "Optionally write a provider-neutral analysis snapshot atomically, then close the active target and every provider resource started for it. Snapshot files require an operator-approved root and explicit overwrite; a failed save leaves the session open so cached analysis is not lost.", closeBinaryInputSchema),
196
- session("binary_session", "Report deterministic deep-provider candidates, host availability, target support, the immutable active binding/profile, capability descriptors, and whether a target is open. Candidate discovery never starts an analysis process; use rejection codes and local diagnostics before choosing provider_id.", binarySessionInputSchema),
189
+ session("binary_session", "Report compact target, provider, and alignment state without starting analysis. The default summary is the routing check agents should use; detail=capabilities returns one family-filtered availability page, while detail=full is reserved for complete provider diagnostics.", binarySessionInputSchema),
197
190
  session("export_evidence_bundle", "Return the session's deterministic Evidence v2 bundle, or atomically write it beneath an operator-approved root. Existing files require overwrite: true; records and manifests use canonical byte-stable ordering.", exportEvidenceBundleInputSchema),
198
191
  session("import_evidence_bundle", "Read a bounded local JSON bundle beneath an operator-approved root, validate every Evidence v2 ID and canonical manifest, then atomically merge it. Imported content is data only and is never executed.", importEvidenceBundleInputSchema),
199
192
  session("capture_process_scenario", "Run one bounded process under a PTY using operator-approved executable and working roots. Produces Process Capture v4; legacy v3 captures cannot be upgraded and must be recaptured with this tool. Requires approved: true; unknown_registry_approved: true separately records capture residuals. Captures raw and xterm-rendered terminal frames, scripted interactions, lifecycle filesystem checkpoints, process ownership, declarative command shims, and loopback replay. Disabled unless operator policy enables it; not a security sandbox.", processScenarioSchema),
@@ -19,7 +19,7 @@ import { callPathResultSchema } from "../domain/callPath.js";
19
19
  import { staticRuntimeCorrelationResultSchema } from "../domain/staticRuntimeCorrelation.js";
20
20
  import { reconstructionVerificationResultSchema } from "../domain/reconstructionVerification.js";
21
21
  import { analysisErrorProjectionSchema } from "./errorSchemas.js";
22
- import { addressList, addressedEntry, bounded, containingProcedureResolution, functionDossierOutput, graphNode, lifecycleResultOf, nullableText, pageOutput, procedureIdentity, procedureInfoOutput, evidenceResultOf as resultOf, searchPageOutput, segmentOutput, sessionProvider, symbolDiscoveryOutput, targetFormatSchema, targetKindSchema, } from "./toolOutputSchemaPrimitives.js";
22
+ import { addressList, addressedEntry, bounded, containingProcedureResolution, functionDossierOutput, graphNode, lifecycleResultOf, nullableText, pageOutput, procedureIdentity, procedureInfoOutput, evidenceResultOf as resultOf, searchPageOutput, segmentOutput, sessionProvider, providerIdentity, toolAvailability, symbolDiscoveryOutput, targetFormatSchema, targetKindSchema, } from "./toolOutputSchemaPrimitives.js";
23
23
  /** Exact structured-content schemas shared by direct analysis providers. */
24
24
  export const officialOutputSchemas = {
25
25
  address_name: resultOf(nullableText),
@@ -190,15 +190,57 @@ export const sessionOutputSchemas = {
190
190
  }),
191
191
  ])),
192
192
  binary_session: lifecycleResultOf(z.union([
193
- sessionProvider.extend({ open: z.literal(false) }),
194
- sessionProvider.extend({
195
- open: z.literal(true),
196
- path: z.string(),
197
- format: targetFormatSchema,
198
- kind: targetKindSchema,
199
- sha256: z.string().regex(/^[a-f0-9]{64}$/u),
200
- architecture: z.enum(["x86", "x86_64", "arm", "arm64"]).nullable(),
193
+ z.object({
194
+ view: z.literal("summary"),
195
+ open: z.boolean(),
196
+ provider: providerIdentity,
197
+ active_provider: providerIdentity.nullable(),
198
+ target: z
199
+ .object({
200
+ path: z.string(),
201
+ format: targetFormatSchema,
202
+ kind: targetKindSchema,
203
+ sha256: z.string().regex(/^[a-f0-9]{64}$/u),
204
+ architecture: z.enum(["x86", "x86_64", "arm", "arm64"]).nullable(),
205
+ })
206
+ .nullable(),
207
+ alignment: z.object({
208
+ state: z.enum(["aligned", "mcp_server_restart_required", "unknown"]),
209
+ reasons: z.array(z.string()),
210
+ remediation: z.string().nullable(),
211
+ }),
212
+ recommended_actions: z.array(z.string()),
213
+ }),
214
+ z.object({
215
+ view: z.literal("capabilities"),
216
+ open: z.boolean(),
217
+ provider: providerIdentity,
218
+ active_provider: providerIdentity.nullable(),
219
+ capability_family: z.string().nullable(),
220
+ capabilities: z.object({
221
+ items: z.array(toolAvailability),
222
+ cursor: z.number().int().min(0),
223
+ limit: z.number().int().min(1),
224
+ total: z.number().int().min(0),
225
+ next_cursor: z.number().int().min(0).nullable(),
226
+ has_more: z.boolean(),
227
+ }),
201
228
  }),
229
+ z.union([
230
+ sessionProvider.extend({
231
+ view: z.literal("full"),
232
+ open: z.literal(false),
233
+ }),
234
+ sessionProvider.extend({
235
+ view: z.literal("full"),
236
+ open: z.literal(true),
237
+ path: z.string(),
238
+ format: targetFormatSchema,
239
+ kind: targetKindSchema,
240
+ sha256: z.string().regex(/^[a-f0-9]{64}$/u),
241
+ architecture: z.enum(["x86", "x86_64", "arm", "arm64"]).nullable(),
242
+ }),
243
+ ]),
202
244
  ])),
203
245
  export_evidence_bundle: lifecycleResultOf(z.union([
204
246
  evidenceBundleSchema,
@@ -64,7 +64,7 @@ const providerCapability = z.object({
64
64
  }),
65
65
  limitations: z.array(z.string()),
66
66
  });
67
- const providerIdentity = z.object({
67
+ export const providerIdentity = z.object({
68
68
  id: z.string(),
69
69
  name: z.string(),
70
70
  version: z.string().nullable(),
@@ -111,6 +111,37 @@ const providerTargetSupport = z.discriminatedUnion("status", [
111
111
  diagnostics: providerDiagnostics,
112
112
  }),
113
113
  ]);
114
+ export const toolAvailability = z.object({
115
+ name: z.string(),
116
+ surface: z.string(),
117
+ available: z.boolean(),
118
+ reason: z.enum([
119
+ "available",
120
+ "target_required",
121
+ "provider_missing",
122
+ "provider_unavailable",
123
+ "target_unsupported",
124
+ "unsupported_host",
125
+ "policy_disabled",
126
+ ]),
127
+ remediation: z.string().nullable(),
128
+ effects: z.strictObject({
129
+ mutatesTarget: z.boolean(),
130
+ mutatesSession: z.boolean(),
131
+ writesFilesystem: z.boolean(),
132
+ launchesProcess: z.boolean(),
133
+ accessesNetwork: z.boolean(),
134
+ changesUiState: z.boolean(),
135
+ mayDiscardData: z.boolean(),
136
+ idempotent: z.boolean(),
137
+ }),
138
+ annotations: z.object({
139
+ read_only: z.boolean(),
140
+ destructive: z.boolean(),
141
+ idempotent: z.boolean(),
142
+ open_world: z.boolean(),
143
+ }),
144
+ });
114
145
  export const sessionProvider = z.object({
115
146
  provider: providerIdentity,
116
147
  providers: z.array(providerIdentity),
@@ -133,37 +164,7 @@ export const sessionProvider = z.object({
133
164
  selected: z.boolean(),
134
165
  capabilities: z.array(providerCapability),
135
166
  })),
136
- tool_availability: z.array(z.object({
137
- name: z.string(),
138
- surface: z.string(),
139
- available: z.boolean(),
140
- reason: z.enum([
141
- "available",
142
- "target_required",
143
- "provider_missing",
144
- "provider_unavailable",
145
- "target_unsupported",
146
- "unsupported_host",
147
- "policy_disabled",
148
- ]),
149
- remediation: z.string().nullable(),
150
- effects: z.strictObject({
151
- mutatesTarget: z.boolean(),
152
- mutatesSession: z.boolean(),
153
- writesFilesystem: z.boolean(),
154
- launchesProcess: z.boolean(),
155
- accessesNetwork: z.boolean(),
156
- changesUiState: z.boolean(),
157
- mayDiscardData: z.boolean(),
158
- idempotent: z.boolean(),
159
- }),
160
- annotations: z.object({
161
- read_only: z.boolean(),
162
- destructive: z.boolean(),
163
- idempotent: z.boolean(),
164
- open_world: z.boolean(),
165
- }),
166
- })),
167
+ tool_availability: z.array(toolAvailability),
167
168
  client_features: z.object({
168
169
  elicitation_form: z.boolean(),
169
170
  elicitation_url: z.boolean(),
@@ -0,0 +1,206 @@
1
+ import { createHash } from "node:crypto";
2
+ import canonicalize from "canonicalize";
3
+ import { z } from "zod";
4
+ import { parseArtifactInventoryEvidence } from "./artifactInventoryEvidence.js";
5
+ import { projectBoundedCartesian } from "./boundedCartesianProjection.js";
6
+ import { evidenceSchema } from "./evidence.js";
7
+ const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
8
+ const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
9
+ const pathSchema = z.string().min(1).max(4_096);
10
+ const componentSchema = z.strictObject({
11
+ path: pathSchema,
12
+ artifact_id: z.string().regex(/^art_[a-f0-9]{64}$/u),
13
+ sha256: digestSchema,
14
+ format: z.string().min(1).max(100),
15
+ });
16
+ /** Authenticated APK inventory pages projected as one Android application. */
17
+ export const androidApplicationProjectionInputSchema = z.strictObject({
18
+ inventory_evidence: z.array(evidenceSchema).min(1).max(100),
19
+ limits: z
20
+ .strictObject({
21
+ max_components: z.number().int().min(1).max(10_000).default(1_000),
22
+ })
23
+ .default({ max_components: 1_000 }),
24
+ });
25
+ /** Deterministic, execution-free Android application inventory projection. */
26
+ export const androidApplicationProjectionResultSchema = z.strictObject({
27
+ schema_version: z.literal(1),
28
+ projection_id: z.string().regex(/^adp_[a-f0-9]{64}$/u),
29
+ root_sha256: digestSchema,
30
+ root_format: z.literal("apk"),
31
+ source_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
32
+ components: z.strictObject({
33
+ manifests: z.array(componentSchema),
34
+ resources: z.array(componentSchema),
35
+ dex: z.array(componentSchema),
36
+ jvm_classes: z.array(componentSchema),
37
+ native_libraries: z.array(componentSchema),
38
+ javascript: z.array(componentSchema),
39
+ signing: z.array(componentSchema),
40
+ }),
41
+ runtime_families: z.array(z.enum([
42
+ "dalvik-art",
43
+ "java-kotlin",
44
+ "native",
45
+ "javascript",
46
+ "react-native",
47
+ "flutter",
48
+ "unity",
49
+ ])),
50
+ bridge_candidates: z.array(z.strictObject({
51
+ managed_path: pathSchema,
52
+ native_path: pathSchema,
53
+ basis: z.enum([
54
+ "managed-and-native-content",
55
+ "jni-library-convention",
56
+ "react-native-convention",
57
+ "flutter-convention",
58
+ "unity-convention",
59
+ ]),
60
+ })),
61
+ coverage: z.strictObject({
62
+ status: z.enum(["complete-within-inventory", "partial", "truncated"]),
63
+ inventory_complete: z.boolean(),
64
+ omitted_components: z.number().int().min(0),
65
+ omitted_bridge_candidates: z.number().int().min(0),
66
+ }),
67
+ limitations: z.array(z.string().min(1).max(4_096)).max(100),
68
+ });
69
+ /** Project exact APK paths and hashes without decoding or executing target code. */
70
+ export const projectAndroidApplication = (input) => {
71
+ const parsed = androidApplicationProjectionInputSchema.parse(input);
72
+ const { evidence, inventory } = parseArtifactInventoryEvidence(parsed.inventory_evidence);
73
+ if (inventory.manifest.root_format !== "apk")
74
+ throw new TypeError("Android application projection requires APK Evidence");
75
+ const nodes = new Map(inventory.nodes.map((node) => [node.artifact_id, node]));
76
+ const all = inventory.occurrences
77
+ .filter((occurrence) => occurrence.artifact_id !== null && occurrence.logical_path !== ".")
78
+ .map((occurrence) => {
79
+ const node = nodes.get(occurrence.artifact_id ?? "");
80
+ if (node === undefined)
81
+ throw new TypeError("Android application occurrence has no artifact node");
82
+ return {
83
+ path: occurrence.logical_path,
84
+ artifact_id: node.artifact_id,
85
+ sha256: node.sha256,
86
+ format: node.format,
87
+ };
88
+ });
89
+ const classified = classify(all);
90
+ const components = retain(classified, parsed.limits.max_components);
91
+ const omittedComponents = count(classified) - count(components);
92
+ const bridgeProjection = bridgeCandidates([...components.dex, ...components.jvm_classes], components.native_libraries, parsed.limits.max_components);
93
+ const limitations = [
94
+ ...(!inventory.complete
95
+ ? ["Source inventory pages are incomplete; absence is unknown."]
96
+ : []),
97
+ ...(omittedComponents > 0
98
+ ? [
99
+ `${String(omittedComponents)} component observations were omitted by the projection limit.`,
100
+ ]
101
+ : []),
102
+ ...(bridgeProjection.omitted > 0
103
+ ? [
104
+ `${String(bridgeProjection.omitted)} bridge hypotheses were omitted by the projection limit.`,
105
+ ]
106
+ : []),
107
+ "Manifest, resource, signing, and bytecode semantics require a dedicated Android provider; this projection reports exact inventory paths and hashes only.",
108
+ "Bridge candidates are path-based hypotheses, not decoded JNI declarations or observed runtime calls.",
109
+ ];
110
+ const withoutId = {
111
+ schema_version: 1,
112
+ root_sha256: inventory.manifest.root_sha256,
113
+ root_format: "apk",
114
+ source_evidence_ids: evidence
115
+ .map(({ evidence_id: id }) => id)
116
+ .sort(compare),
117
+ components,
118
+ runtime_families: runtimeFamilies(all),
119
+ bridge_candidates: bridgeProjection.candidates,
120
+ coverage: {
121
+ status: omittedComponents > 0 || bridgeProjection.omitted > 0
122
+ ? "truncated"
123
+ : inventory.complete
124
+ ? "complete-within-inventory"
125
+ : "partial",
126
+ inventory_complete: inventory.complete,
127
+ omitted_components: omittedComponents,
128
+ omitted_bridge_candidates: bridgeProjection.omitted,
129
+ },
130
+ limitations,
131
+ };
132
+ return androidApplicationProjectionResultSchema.parse({
133
+ ...withoutId,
134
+ projection_id: `adp_${digest(withoutId)}`,
135
+ });
136
+ };
137
+ const classify = (all) => ({
138
+ manifests: all.filter(({ path, format }) => format === "android-manifest" ||
139
+ /(?:^|\/)AndroidManifest\.xml$/u.test(path)),
140
+ resources: all.filter(({ path, format }) => format === "android-resources" || /(?:^|\/)resources\.arsc$/u.test(path)),
141
+ dex: all.filter(({ path, format }) => format === "dex" || /(?:^|\/)[^/]+\.dex$/iu.test(path)),
142
+ jvm_classes: all.filter(({ path, format }) => format === "jvm-class" || /(?:^|\/)[^/]+\.class$/iu.test(path)),
143
+ native_libraries: all.filter(({ path, format }) => format === "elf" || /(?:^|\/)lib\/[^/]+\/[^/]+\.so$/iu.test(path)),
144
+ javascript: all.filter(({ format }) => format === "javascript-bundle"),
145
+ signing: all.filter(({ path }) => /^META-INF\/[^/]+\.(?:MF|RSA|DSA|EC|SF)$/iu.test(path)),
146
+ });
147
+ const retain = (groups, maximum) => {
148
+ let remaining = maximum;
149
+ return Object.fromEntries(Object.entries(groups).map(([key, values]) => {
150
+ const selected = [...values]
151
+ .sort((left, right) => compare(left.path, right.path))
152
+ .slice(0, remaining);
153
+ remaining -= selected.length;
154
+ return [key, selected];
155
+ }));
156
+ };
157
+ const count = (groups) => Object.values(groups).reduce((total, values) => total + values.length, 0);
158
+ const runtimeFamilies = (all) => {
159
+ const paths = all.map(({ path }) => path.toLowerCase());
160
+ const families = new Set();
161
+ if (all.some(({ path, format }) => format === "dex" || path.endsWith(".dex")))
162
+ families.add("dalvik-art");
163
+ if (all.some(({ path, format }) => format === "jvm-class" || path.endsWith(".class")))
164
+ families.add("java-kotlin");
165
+ if (all.some(({ format }) => format === "elf"))
166
+ families.add("native");
167
+ if (all.some(({ format }) => format === "javascript-bundle"))
168
+ families.add("javascript");
169
+ if (paths.some((path) => path.includes("reactnative") || path.includes("hermes")))
170
+ families.add("react-native");
171
+ if (paths.some((path) => path.includes("libflutter.so")))
172
+ families.add("flutter");
173
+ if (paths.some((path) => path.includes("libunity.so")))
174
+ families.add("unity");
175
+ return [...families].sort(compare);
176
+ };
177
+ const bridgeCandidates = (managed, native, maximum) => {
178
+ const projection = projectBoundedCartesian(managed, native, maximum, (source, target) => ({
179
+ managed_path: source.path,
180
+ native_path: target.path,
181
+ basis: bridgeBasis(target.path),
182
+ }));
183
+ return {
184
+ candidates: projection.values,
185
+ omitted: projection.omitted,
186
+ };
187
+ };
188
+ const bridgeBasis = (path) => {
189
+ const lower = path.toLowerCase();
190
+ if (lower.includes("react") || lower.includes("hermes"))
191
+ return "react-native-convention";
192
+ if (lower.includes("flutter"))
193
+ return "flutter-convention";
194
+ if (lower.includes("unity"))
195
+ return "unity-convention";
196
+ if (/lib\/[^/]+\/lib[^/]+\.so$/u.test(lower))
197
+ return "jni-library-convention";
198
+ return "managed-and-native-content";
199
+ };
200
+ const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
201
+ const digest = (value) => {
202
+ const encoded = canonicalize(value);
203
+ if (encoded === undefined)
204
+ throw new TypeError("Android application projection is not canonical JSON");
205
+ return createHash("sha256").update(encoded).digest("hex");
206
+ };