@aragon-agent/cli 0.6.38 → 0.6.39

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 (156) hide show
  1. package/CHANGELOG.md +2523 -2326
  2. package/README.md +3614 -3173
  3. package/dist/agent/controller.d.ts +142 -1
  4. package/dist/agent/controller.js +171 -3
  5. package/dist/agent/reducer.d.ts +41 -1
  6. package/dist/agent/reducer.js +25 -0
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +5 -0
  9. package/dist/cli.js +85 -27
  10. package/dist/commands/builtins.js +13 -1
  11. package/dist/commands/keys-command.d.ts +23 -0
  12. package/dist/commands/keys-command.js +38 -0
  13. package/dist/commands/registry.d.ts +39 -0
  14. package/dist/commands/task-command.d.ts +39 -0
  15. package/dist/commands/task-command.js +102 -0
  16. package/dist/config/cli-commands.d.ts +23 -0
  17. package/dist/config/cli-commands.js +36 -0
  18. package/dist/config/env.js +17 -4
  19. package/dist/config/load.js +6 -1
  20. package/dist/config/schema.d.ts +43 -4
  21. package/dist/config/schema.js +31 -5
  22. package/dist/config/store.js +17 -2
  23. package/dist/diagnostics/doctor.d.ts +9 -0
  24. package/dist/diagnostics/doctor.js +34 -0
  25. package/dist/diagnostics/info.d.ts +71 -0
  26. package/dist/diagnostics/info.js +91 -5
  27. package/dist/exec/bridge/caller-tools.d.ts +96 -0
  28. package/dist/exec/bridge/caller-tools.js +213 -0
  29. package/dist/exec/bridge/decisions.d.ts +78 -0
  30. package/dist/exec/bridge/decisions.js +116 -0
  31. package/dist/exec/bridge/human-answers.d.ts +38 -0
  32. package/dist/exec/bridge/human-answers.js +164 -0
  33. package/dist/exec/bridge/limits.d.ts +68 -0
  34. package/dist/exec/bridge/limits.js +68 -0
  35. package/dist/exec/bridge/ndjson.d.ts +17 -0
  36. package/dist/exec/bridge/ndjson.js +83 -0
  37. package/dist/exec/bridge/pending.d.ts +70 -0
  38. package/dist/exec/bridge/pending.js +132 -0
  39. package/dist/exec/bridge/protocol.d.ts +346 -0
  40. package/dist/exec/bridge/protocol.js +256 -0
  41. package/dist/exec/bridge/server.d.ts +142 -0
  42. package/dist/exec/bridge/server.js +502 -0
  43. package/dist/exec/bridge/source.d.ts +58 -0
  44. package/dist/exec/bridge/source.js +145 -0
  45. package/dist/exec/bridge/writer.d.ts +26 -0
  46. package/dist/exec/bridge/writer.js +156 -0
  47. package/dist/exec/cli-commands.js +30 -2
  48. package/dist/exec/emitter.d.ts +16 -2
  49. package/dist/exec/emitter.js +26 -6
  50. package/dist/exec/events.d.ts +465 -10
  51. package/dist/exec/events.js +47 -1
  52. package/dist/exec/hooks/dispatch.d.ts +76 -0
  53. package/dist/exec/hooks/dispatch.js +169 -0
  54. package/dist/exec/hooks/index.d.ts +83 -0
  55. package/dist/exec/hooks/index.js +205 -0
  56. package/dist/exec/hooks/limits.d.ts +56 -0
  57. package/dist/exec/hooks/limits.js +49 -0
  58. package/dist/exec/hooks/registry.d.ts +49 -0
  59. package/dist/exec/hooks/registry.js +91 -0
  60. package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
  61. package/dist/exec/hooks/tool-wrapper.js +154 -0
  62. package/dist/exec/hooks/turn.d.ts +61 -0
  63. package/dist/exec/hooks/turn.js +59 -0
  64. package/dist/exec/hooks/validate.d.ts +85 -0
  65. package/dist/exec/hooks/validate.js +310 -0
  66. package/dist/exec/index.d.ts +130 -4
  67. package/dist/exec/index.js +501 -10
  68. package/dist/exec/options.d.ts +124 -2
  69. package/dist/exec/options.js +413 -6
  70. package/dist/exec/permission.d.ts +9 -1
  71. package/dist/exec/permission.js +7 -1
  72. package/dist/exec/request/limits.d.ts +51 -0
  73. package/dist/exec/request/limits.js +49 -0
  74. package/dist/exec/request/scope.d.ts +106 -0
  75. package/dist/exec/request/scope.js +149 -0
  76. package/dist/exec/result/limits.d.ts +74 -0
  77. package/dist/exec/result/limits.js +72 -0
  78. package/dist/exec/result/prompt.d.ts +28 -0
  79. package/dist/exec/result/prompt.js +74 -0
  80. package/dist/exec/result/schema.d.ts +99 -0
  81. package/dist/exec/result/schema.js +464 -0
  82. package/dist/exec/result/sink.d.ts +56 -0
  83. package/dist/exec/result/sink.js +50 -0
  84. package/dist/exec/result/tool.d.ts +37 -0
  85. package/dist/exec/result/tool.js +118 -0
  86. package/dist/exec/runner.d.ts +395 -1
  87. package/dist/exec/runner.js +860 -8
  88. package/dist/exec/threads/aggregate.d.ts +34 -0
  89. package/dist/exec/threads/aggregate.js +30 -0
  90. package/dist/exec/threads/capacity.d.ts +23 -0
  91. package/dist/exec/threads/capacity.js +38 -0
  92. package/dist/exec/threads/dispatch-source.d.ts +60 -0
  93. package/dist/exec/threads/dispatch-source.js +106 -0
  94. package/dist/exec/threads/execute-threads.d.ts +53 -0
  95. package/dist/exec/threads/execute-threads.js +234 -0
  96. package/dist/exec/threads/gate.d.ts +42 -0
  97. package/dist/exec/threads/gate.js +81 -0
  98. package/dist/exec/threads/identity.d.ts +36 -0
  99. package/dist/exec/threads/identity.js +67 -0
  100. package/dist/exec/threads/interrupt.d.ts +29 -0
  101. package/dist/exec/threads/interrupt.js +79 -0
  102. package/dist/exec/threads/limits.d.ts +58 -0
  103. package/dist/exec/threads/limits.js +35 -0
  104. package/dist/exec/threads/outcome.d.ts +47 -0
  105. package/dist/exec/threads/outcome.js +26 -0
  106. package/dist/exec/threads/registry.d.ts +81 -0
  107. package/dist/exec/threads/registry.js +427 -0
  108. package/dist/exec/threads/runtime.d.ts +107 -0
  109. package/dist/exec/threads/runtime.js +523 -0
  110. package/dist/exec/threads/thread-source.d.ts +20 -0
  111. package/dist/exec/threads/thread-source.js +134 -0
  112. package/dist/fast/limits.d.ts +54 -0
  113. package/dist/fast/limits.js +54 -0
  114. package/dist/fast/summary-call.d.ts +72 -0
  115. package/dist/fast/summary-call.js +133 -0
  116. package/dist/fast/summary-prompt.d.ts +106 -0
  117. package/dist/fast/summary-prompt.js +320 -0
  118. package/dist/fast/task-summary-types.d.ts +66 -0
  119. package/dist/fast/task-summary-types.js +17 -0
  120. package/dist/logging/logger.d.ts +1 -1
  121. package/dist/proc/limits.d.ts +7 -0
  122. package/dist/proc/limits.js +7 -0
  123. package/dist/proc/output-streams.d.ts +50 -0
  124. package/dist/proc/output-streams.js +54 -0
  125. package/dist/team/runtime.d.ts +9 -0
  126. package/dist/team/runtime.js +8 -0
  127. package/dist/team/subagent.d.ts +15 -0
  128. package/dist/team/subagent.js +45 -1
  129. package/dist/tools/bash-tool.d.ts +10 -0
  130. package/dist/tools/bash-tool.js +71 -6
  131. package/dist/tools/index.d.ts +93 -1
  132. package/dist/tools/index.js +112 -3
  133. package/dist/tools/tool-gate.d.ts +100 -0
  134. package/dist/tools/tool-gate.js +87 -0
  135. package/dist/ui/App.js +224 -11
  136. package/dist/ui/Header.d.ts +28 -16
  137. package/dist/ui/Header.js +21 -19
  138. package/dist/ui/interaction-hints.js +6 -0
  139. package/dist/ui/layout/OverlayFrame.d.ts +18 -0
  140. package/dist/ui/layout/OverlayFrame.js +19 -4
  141. package/dist/ui/layout/header-hints.d.ts +55 -62
  142. package/dist/ui/layout/header-hints.js +59 -166
  143. package/dist/ui/layout/header-left.d.ts +115 -0
  144. package/dist/ui/layout/header-left.js +206 -0
  145. package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
  146. package/dist/ui/layout/shortcut-panel-layout.js +321 -0
  147. package/dist/ui/layout/status-detail-layout.js +5 -1
  148. package/dist/ui/layout/task-summary-cell.d.ts +61 -0
  149. package/dist/ui/layout/task-summary-cell.js +65 -0
  150. package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
  151. package/dist/ui/overlays/HelpOverlay.js +29 -2
  152. package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
  153. package/dist/ui/overlays/ShortcutPanel.js +106 -0
  154. package/dist/ui/shortcuts.d.ts +81 -0
  155. package/dist/ui/shortcuts.js +221 -0
  156. package/package.json +2 -2
@@ -886,8 +886,6 @@ export interface PromptsConfig {
886
886
  * `insert` keeps the user's words; `replace` overwrites them.
887
887
  */
888
888
  insertMode: 'insert' | 'replace';
889
- /** Whether the header's right-hand column may teach `^P prompts`. */
890
- hint: boolean;
891
889
  }
892
890
  export declare const DEFAULT_PROMPTS_CONFIG: PromptsConfig;
893
891
  /**
@@ -899,6 +897,35 @@ export declare const DEFAULT_PROMPTS_CONFIG: PromptsConfig;
899
897
  * what the user has already typed into the composer.
900
898
  */
901
899
  export declare function clampPromptsConfig(raw: unknown): PromptsConfig;
900
+ /**
901
+ * The task label's two switches (tui-shortcut-drawer-and-task-summary §4.8).
902
+ *
903
+ * ONE LEVEL DEEP, like every section beside it: `runConfigGet` splits on a single
904
+ * dot, so `taskSummary.enabled` resolves and a nested key would not (F-18).
905
+ *
906
+ * NEITHER KEY IS ABOUT THE LABEL'S CONTENT. `enabled` is the kill switch: off
907
+ * means no call is made and no cell is drawn. `fallback` decides what happens when
908
+ * the call cannot produce a label - the deterministic verbatim-prefix floor, or
909
+ * nothing at all. Both are BEHAVIOUR, which is why they are here rather than in
910
+ * `fast/limits.ts` (F-13).
911
+ */
912
+ export interface TaskSummaryConfig {
913
+ /** The kill switch. `false` removes the header cell and the automatic call. */
914
+ enabled: boolean;
915
+ /** The deterministic verbatim-prefix floor (D-9). `false` = show nothing instead. */
916
+ fallback: boolean;
917
+ }
918
+ export declare const DEFAULT_TASK_SUMMARY_CONFIG: TaskSummaryConfig;
919
+ /**
920
+ * THE single gate for every `taskSummary` read AND write, like the twelve sections
921
+ * above it.
922
+ *
923
+ * BOTH DEFAULTS ARE `true`, AND THE DIRECTION MATTERS: a corrupt or hand-edited
924
+ * section must not silently switch off a feature the user asked for, and - more
925
+ * importantly - it must not switch off the FLOOR, which is the only thing standing
926
+ * between a failed call and an empty header cell.
927
+ */
928
+ export declare function clampTaskSummaryConfig(raw: unknown): TaskSummaryConfig;
902
929
  /**
903
930
  * Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
904
931
  *
@@ -1061,8 +1088,10 @@ export interface PersistedConfig {
1061
1088
  bash: BashConfig;
1062
1089
  /** Background jobs and bounded waiting. The ELEVENTH; scalars only (see JobsConfig). */
1063
1090
  jobs: JobsConfig;
1064
- /** Quick prompts. The TWELFTH; three scalars (see PromptsConfig). */
1091
+ /** Quick prompts. The TWELFTH; two scalars (see PromptsConfig). */
1065
1092
  prompts: PromptsConfig;
1093
+ /** The header task label. The THIRTEENTH; two scalars (see TaskSummaryConfig). */
1094
+ taskSummary: TaskSummaryConfig;
1066
1095
  }
1067
1096
  export declare const DEFAULT_CONFIG: PersistedConfig;
1068
1097
  /**
@@ -1406,7 +1435,7 @@ export interface CliConfig {
1406
1435
  * scalars, one level deep.
1407
1436
  *
1408
1437
  * ON `CliConfig` AS WELL AS `PersistedConfig`, for the reason every section above
1409
- * records: `App` reads `cfg.prompts?.enabled` / `.hint` / `.insertMode` from a
1438
+ * records: `App` reads `cfg.prompts?.enabled` / `.insertMode` from a
1410
1439
  * `CliConfig`, so a key added to the persisted shape alone would round-trip
1411
1440
  * through the `config` commands, appear in `config list`, and change nothing.
1412
1441
  *
@@ -1419,6 +1448,16 @@ export interface CliConfig {
1419
1448
  * disabled or destructive one.
1420
1449
  */
1421
1450
  prompts?: PromptsConfig;
1451
+ /**
1452
+ * The header task label (tui-shortcut-drawer-and-task-summary §4.8). The
1453
+ * THIRTEENTH nested object; two scalars, one level deep.
1454
+ *
1455
+ * OPTIONAL HERE FOR THE SAME REASON AS `prompts` ABOVE: `CliConfig` is built by
1456
+ * hand in many fixtures, and every reader uses `!== false`, so a missing section
1457
+ * behaves exactly like a default session (label on, floor on) rather than like a
1458
+ * switched-off feature.
1459
+ */
1460
+ taskSummary?: TaskSummaryConfig;
1422
1461
  /**
1423
1462
  * Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
1424
1463
  *
@@ -966,7 +966,6 @@ export function clampJobsConfig(raw) {
966
966
  export const DEFAULT_PROMPTS_CONFIG = {
967
967
  enabled: true,
968
968
  insertMode: 'insert',
969
- hint: true,
970
969
  };
971
970
  const PROMPT_INSERT_MODES = ['insert', 'replace'];
972
971
  /**
@@ -985,7 +984,26 @@ export function clampPromptsConfig(raw) {
985
984
  insertMode: PROMPT_INSERT_MODES.includes(mode)
986
985
  ? mode
987
986
  : DEFAULT_PROMPTS_CONFIG.insertMode,
988
- hint: bool(src.hint, DEFAULT_PROMPTS_CONFIG.hint),
987
+ };
988
+ }
989
+ export const DEFAULT_TASK_SUMMARY_CONFIG = {
990
+ enabled: true,
991
+ fallback: true,
992
+ };
993
+ /**
994
+ * THE single gate for every `taskSummary` read AND write, like the twelve sections
995
+ * above it.
996
+ *
997
+ * BOTH DEFAULTS ARE `true`, AND THE DIRECTION MATTERS: a corrupt or hand-edited
998
+ * section must not silently switch off a feature the user asked for, and - more
999
+ * importantly - it must not switch off the FLOOR, which is the only thing standing
1000
+ * between a failed call and an empty header cell.
1001
+ */
1002
+ export function clampTaskSummaryConfig(raw) {
1003
+ const src = (raw && typeof raw === 'object' ? raw : {});
1004
+ return {
1005
+ enabled: bool(src.enabled, DEFAULT_TASK_SUMMARY_CONFIG.enabled),
1006
+ fallback: bool(src.fallback, DEFAULT_TASK_SUMMARY_CONFIG.fallback),
989
1007
  };
990
1008
  }
991
1009
  /**
@@ -1093,14 +1111,22 @@ export const DEFAULT_CONFIG = {
1093
1111
  // rather than quietly disabling something.
1094
1112
  jobs: DEFAULT_JOBS_CONFIG,
1095
1113
  // And one section later again, for the seventh time, and this one has BOTH
1096
- // failure directions at once: `prompts.enabled` and `prompts.hint` are `true` by
1097
- // default, so a `config.json` predating this feature would resolve them to
1098
- // `undefined` (falsy) and arrive with the key and its hint silently off - while
1114
+ // failure directions at once: `prompts.enabled` is `true` by default, so a
1115
+ // `config.json` predating this feature would resolve it to `undefined` (falsy)
1116
+ // and arrive with the key silently off - while
1099
1117
  // `prompts.insertMode` would be `undefined`, which is neither `'insert'` nor
1100
1118
  // `'replace'` and would make `planPromptFill` fall through to its `replace`
1101
1119
  // branch. That branch discards the user's own words, so the miss would be a data
1102
1120
  // loss rather than a missing hint.
1103
1121
  prompts: DEFAULT_PROMPTS_CONFIG,
1122
+ // And one section later again, for the eighth time - and this one has a silent
1123
+ // DATA-LOSS-shaped miss: `taskSummary.enabled` and `.fallback` are both `true` by
1124
+ // default, and `App` reads `cfg.taskSummary?.fallback !== false` at call time. A
1125
+ // `config.json` predating this feature would resolve them to `undefined`, which
1126
+ // happens to behave like the defaults in those two readers but NOT in
1127
+ // `config list`, so the file would report a section it never wrote. One line
1128
+ // here, like the seven above it.
1129
+ taskSummary: DEFAULT_TASK_SUMMARY_CONFIG,
1104
1130
  };
1105
1131
  /**
1106
1132
  * Runtime state that used to be stored in `config.json` by mistake
@@ -17,7 +17,7 @@ import { getConfigPath, getSessionsDir } from './app-paths.js';
17
17
  import { registerProfileSecrets, registerSecretsFrom } from '../logging/secret-registry.js';
18
18
  import { adaptLegacyConnectionPatch, ModelProfileConfigError, readModelSettingsDisk } from './model-profile-store.js';
19
19
  import { normalizeModelProfiles } from './model-profiles.js';
20
- import { clampBashConfig, clampJobsConfig, clampPromptsConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
20
+ import { clampBashConfig, clampJobsConfig, clampPromptsConfig, clampTaskSummaryConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
21
21
  /**
22
22
  * Normalize `maxTokens` for both directions of the config file.
23
23
  *
@@ -200,6 +200,14 @@ export function normalizePersistedConfig(input) {
200
200
  // `'insert'` nor `'replace'`, so a fill into a non-empty draft would take the
201
201
  // branch that REPLACES what the user typed.
202
202
  prompts: clampPromptsConfig({ ...DEFAULT_CONFIG.prompts, ...(partial.prompts ?? {}) }),
203
+ // And the thirteenth, on the same line and for the same reason. Without it a
204
+ // `config.json` predating the header label resolves the whole section to
205
+ // `undefined`, and the two readers disagree: `App`'s `!== false` checks would
206
+ // treat it as ON while `config list` would report nothing at all.
207
+ taskSummary: clampTaskSummaryConfig({
208
+ ...DEFAULT_CONFIG.taskSummary,
209
+ ...(partial.taskSummary ?? {}),
210
+ }),
203
211
  };
204
212
  }
205
213
  /**
@@ -308,11 +316,18 @@ export function mergePersistedConfig(current, patch) {
308
316
  // user would never learn that their privacy switch had been turned back on.
309
317
  jobs: clampJobsConfig({ ...current.jobs, ...(patch.jobs ?? {}) }),
310
318
  // The twelfth section, and the seventh time this file records the same trap.
311
- // `aragon config set prompts.hint false` sends `{ prompts: { hint: false } }`;
319
+ // `aragon config set prompts.insertMode replace` sends `{ prompts: { insertMode } }`;
312
320
  // without this line the shallow top-level spread REPLACES the section and
313
321
  // `insertMode` is written absent - which reads back as neither `'insert'` nor
314
322
  // `'replace'`, i.e. the next fill overwrites the user's own draft.
315
323
  prompts: clampPromptsConfig({ ...current.prompts, ...(patch.prompts ?? {}) }),
324
+ // The thirteenth section, and the eighth time this file records the same trap.
325
+ // `aragon config set taskSummary.fallback false` sends
326
+ // `{ taskSummary: { fallback: false } }`; without this line the shallow spread
327
+ // REPLACES the section and `enabled` is written absent - and the one direction
328
+ // that must never happen is a FAILED summary call landing on "show nothing"
329
+ // because a switch the user never touched defaulted away.
330
+ taskSummary: clampTaskSummaryConfig({ ...current.taskSummary, ...(patch.taskSummary ?? {}) }),
316
331
  };
317
332
  return merged;
318
333
  }
@@ -27,3 +27,12 @@ export interface DoctorOptions {
27
27
  probe?: boolean;
28
28
  }
29
29
  export declare function runDoctorChecks(flags: CliFlags, opts?: DoctorOptions): Promise<DoctorReport>;
30
+ /**
31
+ * Is the optional JSON-Schema validator importable?
32
+ *
33
+ * ASKED ONCE, HERE, AND REUSED BY `aragon info` - because "how strict is my result
34
+ * schema validated" is a property of this MACHINE, not of the contract, and there must
35
+ * be exactly one answer to it. `createRequire().resolve()` rather than `import('ajv')`
36
+ * because `info` is synchronous: resolution is the question, not loading.
37
+ */
38
+ export declare function hasAjvValidator(): boolean;
@@ -10,6 +10,7 @@
10
10
  * the caller guess, which is the state this command exists to end.
11
11
  */
12
12
  import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
13
+ import { createRequire } from 'node:module';
13
14
  import { join } from 'node:path';
14
15
  import process from 'node:process';
15
16
  import { initProviders, ModelRegistry } from '@aragon-agent/core';
@@ -26,10 +27,43 @@ export async function runDoctorChecks(flags, opts = {}) {
26
27
  checks.push(checkProviderKey(config));
27
28
  checks.push(checkModel(config));
28
29
  checks.push(checkSessionsDir());
30
+ checks.push(checkValidationStrength());
29
31
  if (opts.probe)
30
32
  checks.push(await probeProvider(config));
31
33
  return { ok: checks.every((c) => c.verdict !== 'fail'), checks };
32
34
  }
35
+ /**
36
+ * Is the optional JSON-Schema validator importable?
37
+ *
38
+ * ASKED ONCE, HERE, AND REUSED BY `aragon info` - because "how strict is my result
39
+ * schema validated" is a property of this MACHINE, not of the contract, and there must
40
+ * be exactly one answer to it. `createRequire().resolve()` rather than `import('ajv')`
41
+ * because `info` is synchronous: resolution is the question, not loading.
42
+ */
43
+ export function hasAjvValidator() {
44
+ try {
45
+ createRequire(import.meta.url).resolve('ajv');
46
+ return true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ /**
53
+ * `ajv: present | absent` - a `pass`, never a `warn`.
54
+ *
55
+ * BOTH ANSWERS ARE CORRECT INSTALLS. `ajv` is an OPTIONAL dependency of Core, and its
56
+ * absence degrades validation honestly (the CLI's own belt still enforces `required`,
57
+ * recursive `properties`, `items` and `additionalProperties:false`). The check exists
58
+ * because the STRENGTH differs and nothing else in the product says so out loud.
59
+ */
60
+ function checkValidationStrength() {
61
+ if (hasAjvValidator()) {
62
+ return pass('ajv', 'present: full JSON Schema keywords are enforced');
63
+ }
64
+ return pass('ajv', 'absent: required/properties/items/additionalProperties are still enforced by the ' +
65
+ 'built-in validator; pattern/format/multipleOf are not');
66
+ }
33
67
  function pass(name, detail) {
34
68
  return { name, verdict: 'pass', detail, remedy: '' };
35
69
  }
@@ -26,5 +26,76 @@ export interface ExecInfo {
26
26
  inputFormats: string[];
27
27
  permissionModes: string[];
28
28
  features: string[];
29
+ /**
30
+ * The bridge protocol, as data (headless-host-bridge section 4.6).
31
+ *
32
+ * THE POINT OF THIS BLOCK IS THAT A CALLER SHOULD NOT HAVE TO PROBE. The
33
+ * protocol version, which decision kinds exist, which tool names are gated and
34
+ * the limits it will be held to are all answered here, because the alternative
35
+ * is "send a frame and see whether an error comes back" - discovery by damage.
36
+ */
37
+ bridge: {
38
+ protocolVersion: number;
39
+ capabilities: string[];
40
+ decisionKinds: string[];
41
+ gatedTools: string[];
42
+ limits: {
43
+ maxCallerTools: number;
44
+ maxFrameBytes: number;
45
+ defaultDecisionTimeoutMs: number;
46
+ };
47
+ /**
48
+ * The per-request ceilings (headless-request-scope-and-typed-result section 4.6),
49
+ * BESIDE `limits` rather than inside it: `limits` is what one FRAME may carry,
50
+ * these bound one REQUEST. Both read their own `*_LIMITS` constant, so neither
51
+ * restates a number the enforcement does not use.
52
+ */
53
+ requestLimits: {
54
+ maxRequestTurns: number;
55
+ maxRequestTimeoutMs: number;
56
+ };
57
+ /**
58
+ * The thread ceilings (headless-threads-and-lifecycle-hooks section 4.6).
59
+ *
60
+ * A SIBLING OF `limits` / `requestLimits` RATHER THAN A MEMBER, because the three answer
61
+ * three different questions: what one frame carries, what one request may spend, and how
62
+ * many conversations this process can host.
63
+ */
64
+ threadLimits: {
65
+ maxOpenThreads: number;
66
+ defaultMaxOpenThreads: number;
67
+ defaultConcurrency: number;
68
+ maxThreadIdLength: number;
69
+ };
70
+ /** The hook ceilings and the four points, so a wrapper need not probe for them. */
71
+ hookLimits: {
72
+ maxHooks: number;
73
+ maxHooksPerPoint: number;
74
+ defaultTimeoutMs: number;
75
+ minTimeoutMs: number;
76
+ maxTimeoutMs: number;
77
+ allowModeMaxTimeoutMs: number;
78
+ points: string[];
79
+ };
80
+ };
81
+ /**
82
+ * The structured-result contract, as data (section 4.6).
83
+ *
84
+ * A NEW BLOCK, so an older consumer simply ignores it. It exists because the
85
+ * STRENGTH of the validation is machine-dependent (`ajv` is an optional dependency
86
+ * of Core), and "which strength am I on" is not something a caller can discover by
87
+ * sending a frame.
88
+ */
89
+ result: {
90
+ schemaBytes: number;
91
+ schemaDepth: number;
92
+ schemaRefs: number;
93
+ validationNodes: number;
94
+ maxViolations: number;
95
+ valueBytes: number;
96
+ defaultRepairTurns: number;
97
+ maxRepairTurns: number;
98
+ ajv: boolean;
99
+ };
29
100
  }
30
101
  export declare function buildInfo(flags: CliFlags, version: string): ExecInfo;
@@ -15,8 +15,15 @@ import { readFileSync } from 'node:fs';
15
15
  import process from 'node:process';
16
16
  import { getConfigPath, getSessionsDir, getUserDataDir } from '../config/app-paths.js';
17
17
  import { loadConfig, makeGetApiKey } from '../config/load.js';
18
- import { HOST_TOOL_NAMES } from '../tools/index.js';
19
- import { EXEC_SCHEMA_VERSION } from '../exec/events.js';
18
+ import { CALLER_GATED_TOOLS, HOST_TOOL_NAMES } from '../tools/index.js';
19
+ import { BRIDGE_CAPABILITIES, BRIDGE_PROTOCOL_VERSION } from '../exec/bridge/protocol.js';
20
+ import { BRIDGE_LIMITS } from '../exec/bridge/limits.js';
21
+ import { EXEC_SCHEMA_VERSION, execCapabilities } from '../exec/events.js';
22
+ import { RESULT_LIMITS } from '../exec/result/limits.js';
23
+ import { REQUEST_LIMITS } from '../exec/request/limits.js';
24
+ import { THREAD_LIMITS } from '../exec/threads/limits.js';
25
+ import { HOOK_LIMITS, HOOK_POINTS } from '../exec/hooks/limits.js';
26
+ import { hasAjvValidator } from './doctor.js';
20
27
  /**
21
28
  * Everything this build can do, as names a wrapper can test for.
22
29
  *
@@ -52,11 +59,90 @@ export function buildInfo(flags, version) {
52
59
  // `HOST_TOOL_NAMES` means and exactly what `--allow-tool` validates against.
53
60
  tools: [...HOST_TOOL_NAMES],
54
61
  outputFormats: ['text', 'json', 'stream-json'],
55
- inputFormats: ['text', 'stream-json'],
56
- permissionModes: ['auto', 'plan', 'strict'],
57
- features: [...FEATURES],
62
+ // THESE TWO LINES ARE THE ANSWER TO "WHAT DOES THIS BUILD ACCEPT", and this is
63
+ // the only place that answer exists in machine-readable form. Leaving them at
64
+ // three and three while `system/init` announces `bridge` and a run accepts
65
+ // `--permission-mode ask` would be two machine-readable answers that contradict
66
+ // each other, with nothing to tell a caller which one is true.
67
+ inputFormats: ['text', 'stream-json', 'bridge'],
68
+ permissionModes: ['auto', 'plan', 'strict', 'ask'],
69
+ features: buildFeatures(),
70
+ bridge: {
71
+ protocolVersion: BRIDGE_PROTOCOL_VERSION,
72
+ capabilities: [...BRIDGE_CAPABILITIES],
73
+ decisionKinds: ['tool', 'questions', 'plan', 'skill_install'],
74
+ // FROM THE CONSTANT THE GATE USES, so a caller never has to read the docs to
75
+ // learn that `skill_create` also asks it - and so this list cannot drift from
76
+ // the one enforcement is built on.
77
+ gatedTools: [...CALLER_GATED_TOOLS],
78
+ limits: {
79
+ maxCallerTools: BRIDGE_LIMITS.maxCallerTools,
80
+ maxFrameBytes: BRIDGE_LIMITS.maxFrameBytes,
81
+ defaultDecisionTimeoutMs: BRIDGE_LIMITS.defaultDecisionTimeoutMs,
82
+ },
83
+ requestLimits: {
84
+ maxRequestTurns: REQUEST_LIMITS.maxRequestTurns,
85
+ maxRequestTimeoutMs: REQUEST_LIMITS.maxRequestTimeoutMs,
86
+ },
87
+ // THREADS AND HOOKS (headless-threads-and-lifecycle-hooks section 4.6). REFERENCED
88
+ // from `THREAD_LIMITS` / `HOOK_LIMITS`, never restated: this block and the runtime's
89
+ // clamps have to be the same numbers, and a second literal is how they stop being.
90
+ threadLimits: {
91
+ maxOpenThreads: THREAD_LIMITS.maxOpenThreads,
92
+ defaultMaxOpenThreads: THREAD_LIMITS.defaultMaxOpenThreads,
93
+ defaultConcurrency: THREAD_LIMITS.defaultConcurrency,
94
+ maxThreadIdLength: THREAD_LIMITS.maxThreadIdLength,
95
+ },
96
+ hookLimits: {
97
+ maxHooks: HOOK_LIMITS.maxHooks,
98
+ maxHooksPerPoint: HOOK_LIMITS.maxHooksPerPoint,
99
+ defaultTimeoutMs: HOOK_LIMITS.defaultTimeoutMs,
100
+ minTimeoutMs: HOOK_LIMITS.minTimeoutMs,
101
+ maxTimeoutMs: HOOK_LIMITS.maxTimeoutMs,
102
+ allowModeMaxTimeoutMs: HOOK_LIMITS.allowModeMaxTimeoutMs,
103
+ points: [...HOOK_POINTS],
104
+ },
105
+ },
106
+ result: {
107
+ schemaBytes: RESULT_LIMITS.maxSchemaBytes,
108
+ schemaDepth: RESULT_LIMITS.maxSchemaDepth,
109
+ schemaRefs: RESULT_LIMITS.maxSchemaRefs,
110
+ validationNodes: RESULT_LIMITS.maxValidationNodes,
111
+ maxViolations: RESULT_LIMITS.maxViolations,
112
+ valueBytes: RESULT_LIMITS.maxResultValueBytes,
113
+ defaultRepairTurns: RESULT_LIMITS.defaultRepairTurns,
114
+ maxRepairTurns: RESULT_LIMITS.maxRepairTurns,
115
+ ajv: hasAjvValidator(),
116
+ },
58
117
  };
59
118
  }
119
+ /**
120
+ * `features` = this build's own names PLUS the capability strings of one
121
+ * fully-featured run.
122
+ *
123
+ * DERIVED FROM `execCapabilities()`, NOT WRITTEN OUT BY HAND (review P0-4). The whole
124
+ * point of `info` is that a caller can read it BEFORE deciding which frames to send;
125
+ * if this list and `system/init.capabilities` were two hand-maintained literals, the
126
+ * first one to gain a member would silently make that strategy wrong.
127
+ * `{ bridge: true, structuredResult: true }` is the right argument here because this
128
+ * command answers "what can this build do", not "what did one run do".
129
+ */
130
+ function buildFeatures() {
131
+ const features = [...FEATURES];
132
+ for (const capability of execCapabilities({
133
+ bridge: true,
134
+ structuredResult: true,
135
+ // THE SUPERSET ARGUMENT AGAIN: `info` answers "what can this build do", so the two new
136
+ // capabilities are declared here and only HERE - a run that does not open `--threads`
137
+ // still reports them in its own `init` as absent, from the same derivation point.
138
+ threads: true,
139
+ hooks: true,
140
+ })) {
141
+ if (!features.includes(capability))
142
+ features.push(capability);
143
+ }
144
+ return features;
145
+ }
60
146
  /**
61
147
  * The installed `@aragon-agent/core` version, or `'unknown'`.
62
148
  *
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Caller-served tools: what a `tool_register` frame becomes, and how it runs.
3
+ *
4
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
5
+ *
6
+ * ## I-12: THESE TOOLS LIVE OUTSIDE THE HOST'S JURISDICTION, ON PURPOSE
7
+ *
8
+ * Everything below is built HERE and handed straight to the registry. That means a
9
+ * caller tool is NOT covered by the plan gate, NOT covered by a skill's
10
+ * `allowed-tools` ceiling, and has no job-notice tail - because it never passes
11
+ * through `createBuiltinTools`, which is where all three wrappers are applied.
12
+ *
13
+ * This is a decision, not an oversight, and it is written down because the next
14
+ * person to read this file will otherwise assume the wrappers are there. The
15
+ * defences that DO apply are: plan mode refuses the whole `tool_register` frame
16
+ * (`plan_mode_read_only`), and a caller tool may not shadow any host, team or
17
+ * skill tool name (`caller_tool_conflict`). Both are refusals at registration
18
+ * time, which is the only moment this module can still see the caller's intent.
19
+ *
20
+ * ## Why `validateCallerTools` REFUSES WHOLE FRAMES
21
+ *
22
+ * A partially accepted registration would leave the registry holding a mixture of
23
+ * two declarations - the caller's old `db_query` and its new `db_write` - with
24
+ * nothing anywhere saying which of the two the model is now using. The caller's
25
+ * natural recovery ("just send the fixed list again") only works if the previous
26
+ * frame left no trace, so every check below runs BEFORE the first write.
27
+ */
28
+ import { type AgentTool, type ToolResult } from '@aragon-agent/core';
29
+ import type { BridgeServer } from './server.js';
30
+ import type { CallerToolSpec } from './protocol.js';
31
+ export interface CallerToolValidation {
32
+ ok: true;
33
+ specs: CallerToolSpec[];
34
+ /** Non-fatal notes for `ack.warnings` - a clipped description, so far. */
35
+ warnings: string[];
36
+ }
37
+ export interface CallerToolRejection {
38
+ ok: false;
39
+ code: string;
40
+ message: string;
41
+ }
42
+ export type CallerToolValidationResult = CallerToolValidation | CallerToolRejection;
43
+ /**
44
+ * Narrow whatever arrived in `tool_register.tools` into usable specs.
45
+ *
46
+ * `reserved` is a PARAMETER rather than an import so the caller decides what a
47
+ * name may not collide with - and so the test can hand over a two-name set instead
48
+ * of modelling the whole host.
49
+ */
50
+ export declare function validateCallerTools(rawTools: readonly unknown[], reserved: ReadonlySet<string>): CallerToolValidationResult;
51
+ /**
52
+ * The effective ceiling for one call: `min(spec.timeoutMs, --caller-tool-timeout)`.
53
+ *
54
+ * MIN, NOT MAX, because both numbers are ceilings on the same thing: the caller's
55
+ * own declaration is a promise about its implementation ("this query takes at most
56
+ * 20 s") and the flag is the run's budget. Taking the larger of the two would let
57
+ * a declared 600 s override a 10 s budget.
58
+ */
59
+ export declare function resolveCallerToolTimeout(spec: CallerToolSpec | undefined, defaultTimeoutMs: number): number;
60
+ /**
61
+ * Turn specs into `AgentTool`s that round-trip through the server.
62
+ *
63
+ * EVERY PATH RETURNS A `ToolResult`. Nothing here throws: `ToolExecutor` would
64
+ * convert a throw into an error result of its own wording, and that wording says
65
+ * nothing about which of the four refusals happened.
66
+ */
67
+ export declare function createCallerTools(specs: readonly CallerToolSpec[], server: BridgeServer, defaultTimeoutMs: number): AgentTool[];
68
+ /**
69
+ * Coerce a `tool_result.content` into text, or refuse it.
70
+ *
71
+ * TEXT ONLY, THIS ROUND. An image part would need `imageResult` and the provider's
72
+ * multimodal path, which is a separate feature with its own budget questions; a
73
+ * half-support that silently dropped the image would be worse than a named
74
+ * refusal (`bridge_unsupported_content`).
75
+ */
76
+ export declare function contentToText(content: unknown): {
77
+ ok: true;
78
+ text: string;
79
+ } | {
80
+ ok: false;
81
+ };
82
+ /**
83
+ * Clip to `maxCallerToolResultBytes` and SAY SO.
84
+ *
85
+ * The footer is not decoration: `ToolExecutor` already clips model-visible output
86
+ * at its own ceiling, so a silent second clip here would look like the tool
87
+ * returned less than it did, and the model would draw conclusions from a truncated
88
+ * table without being told it was truncated.
89
+ */
90
+ export declare function clipCallerToolText(text: string): string;
91
+ /** Build the result a `tool_result` frame becomes, or a named refusal. */
92
+ export declare function callerToolResult(content: unknown, isError: boolean): ToolResult;
93
+ /** Thrown only so `server.ts` can map it to one error code in one place. */
94
+ export declare class UnsupportedContentError extends Error {
95
+ constructor();
96
+ }