@sentropic/h2a-cli 0.24.0 → 0.26.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 (78) hide show
  1. package/dist/bin.js +4 -1
  2. package/dist/bin.js.map +1 -1
  3. package/dist/cli-contract.d.ts.map +1 -1
  4. package/dist/cli-contract.js +33 -1
  5. package/dist/cli-contract.js.map +1 -1
  6. package/dist/cli.d.ts +16 -0
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +294 -16
  9. package/dist/cli.js.map +1 -1
  10. package/dist/hosts/plugin.d.ts +14 -2
  11. package/dist/hosts/plugin.d.ts.map +1 -1
  12. package/dist/hosts/plugin.js +23 -0
  13. package/dist/hosts/plugin.js.map +1 -1
  14. package/dist/index.d.ts +4 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/runtime/drive/index.d.ts +84 -0
  19. package/dist/runtime/drive/index.d.ts.map +1 -1
  20. package/dist/runtime/drive/index.js +325 -22
  21. package/dist/runtime/drive/index.js.map +1 -1
  22. package/dist/runtime/drumbeat/relaunchers.d.ts +5 -1
  23. package/dist/runtime/drumbeat/relaunchers.d.ts.map +1 -1
  24. package/dist/runtime/drumbeat/relaunchers.js +12 -6
  25. package/dist/runtime/drumbeat/relaunchers.js.map +1 -1
  26. package/dist/runtime/local-files/store.d.ts +14 -1
  27. package/dist/runtime/local-files/store.d.ts.map +1 -1
  28. package/dist/runtime/local-files/store.js +141 -1
  29. package/dist/runtime/local-files/store.js.map +1 -1
  30. package/dist/runtime/mcp-http/app.d.ts +12 -0
  31. package/dist/runtime/mcp-http/app.d.ts.map +1 -0
  32. package/dist/runtime/mcp-http/app.js +64 -0
  33. package/dist/runtime/mcp-http/app.js.map +1 -0
  34. package/dist/runtime/mcp-http/hosted-mcp-server.d.ts +14 -0
  35. package/dist/runtime/mcp-http/hosted-mcp-server.d.ts.map +1 -0
  36. package/dist/runtime/mcp-http/hosted-mcp-server.js +38 -0
  37. package/dist/runtime/mcp-http/hosted-mcp-server.js.map +1 -0
  38. package/dist/runtime/mcp-http/index.d.ts +14 -0
  39. package/dist/runtime/mcp-http/index.d.ts.map +1 -0
  40. package/dist/runtime/mcp-http/index.js +14 -0
  41. package/dist/runtime/mcp-http/index.js.map +1 -0
  42. package/dist/runtime/mcp-http/main.d.ts +2 -0
  43. package/dist/runtime/mcp-http/main.d.ts.map +1 -0
  44. package/dist/runtime/mcp-http/main.js +14 -0
  45. package/dist/runtime/mcp-http/main.js.map +1 -0
  46. package/dist/runtime/mcp-http/oauth/config.d.ts +33 -0
  47. package/dist/runtime/mcp-http/oauth/config.d.ts.map +1 -0
  48. package/dist/runtime/mcp-http/oauth/config.js +31 -0
  49. package/dist/runtime/mcp-http/oauth/config.js.map +1 -0
  50. package/dist/runtime/mcp-http/oauth/crypto.d.ts +5 -0
  51. package/dist/runtime/mcp-http/oauth/crypto.d.ts.map +1 -0
  52. package/dist/runtime/mcp-http/oauth/crypto.js +19 -0
  53. package/dist/runtime/mcp-http/oauth/crypto.js.map +1 -0
  54. package/dist/runtime/mcp-http/oauth/file-store.d.ts +40 -0
  55. package/dist/runtime/mcp-http/oauth/file-store.d.ts.map +1 -0
  56. package/dist/runtime/mcp-http/oauth/file-store.js +101 -0
  57. package/dist/runtime/mcp-http/oauth/file-store.js.map +1 -0
  58. package/dist/runtime/mcp-http/oauth/hono-oauth-router.d.ts +5 -0
  59. package/dist/runtime/mcp-http/oauth/hono-oauth-router.d.ts.map +1 -0
  60. package/dist/runtime/mcp-http/oauth/hono-oauth-router.js +83 -0
  61. package/dist/runtime/mcp-http/oauth/hono-oauth-router.js.map +1 -0
  62. package/dist/runtime/mcp-http/oauth/redirect-uri.d.ts +11 -0
  63. package/dist/runtime/mcp-http/oauth/redirect-uri.d.ts.map +1 -0
  64. package/dist/runtime/mcp-http/oauth/redirect-uri.js +29 -0
  65. package/dist/runtime/mcp-http/oauth/redirect-uri.js.map +1 -0
  66. package/dist/runtime/mcp-http/oauth/single-tenant-provider.d.ts +68 -0
  67. package/dist/runtime/mcp-http/oauth/single-tenant-provider.d.ts.map +1 -0
  68. package/dist/runtime/mcp-http/oauth/single-tenant-provider.js +238 -0
  69. package/dist/runtime/mcp-http/oauth/single-tenant-provider.js.map +1 -0
  70. package/dist/runtime/mcp-http/readonly-allowlist.d.ts +24 -0
  71. package/dist/runtime/mcp-http/readonly-allowlist.d.ts.map +1 -0
  72. package/dist/runtime/mcp-http/readonly-allowlist.js +43 -0
  73. package/dist/runtime/mcp-http/readonly-allowlist.js.map +1 -0
  74. package/dist/runtime/mcp-http/serve.d.ts +30 -0
  75. package/dist/runtime/mcp-http/serve.d.ts.map +1 -0
  76. package/dist/runtime/mcp-http/serve.js +53 -0
  77. package/dist/runtime/mcp-http/serve.js.map +1 -0
  78. package/package.json +6 -2
package/dist/cli.js CHANGED
@@ -36,19 +36,20 @@
36
36
  * The full machine-readable manifest lives in `./cli-contract.ts`
37
37
  * (`H2A_CLI_VERB_CONTRACTS`). Human-readable reference: `docs/cli-contract.md`.
38
38
  */
39
+ import { spawnSync } from "node:child_process";
39
40
  import { generateKeyPairSync } from "node:crypto";
40
41
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
41
42
  import { homedir } from "node:os";
42
43
  import { dirname, join, resolve as resolvePath } from "node:path";
43
44
  import { fileURLToPath } from "node:url";
44
- import { H2A_ATTESTER_COMPREHENSION_RIGHT, H2A_COMPREHENSION_ATTESTATION_BODY_KIND, H2A_DECLARATION_INTERET_BODY_KIND, H2A_ORG_MANIFEST_FILENAME, H2A_ORG_PROPOSAL_BODY_KIND, H2A_ORG_RATIFIED_BODY_KIND, H2A_ROLES, H2A_WORK_STATUSES, auditNhiPosture, buildComprehensionAttestation, canAttestComprehension, computeHash, createEnvelope, diffOrgManifest, effectiveOrgInstances, isComprehensionAttestation, nhiAttestationEnvelope, nhiInventory, nhiTrustBundle, orgAssignmentEnvelope, parseOrgManifest, signCanonical, signEnvelope, subagentAddress, validateOrgManifest, verifyComprehensionAttestation } from "@sentropic/h2a";
45
+ import { H2A_ATTESTER_COMPREHENSION_RIGHT, H2A_COMPREHENSION_ATTESTATION_BODY_KIND, H2A_DECLARATION_INTERET_BODY_KIND, H2A_ORG_MANIFEST_FILENAME, H2A_ORG_PROPOSAL_BODY_KIND, H2A_ORG_RATIFIED_BODY_KIND, H2A_ROLES, H2A_WORK_STATUSES, auditNhiPosture, buildComprehensionAttestation, canAttestComprehension, checkEnvelopeFreshness, computeHash, createEnvelope, diffOrgManifest, effectiveOrgInstances, isComprehensionAttestation, nhiAttestationEnvelope, nhiInventory, nhiTrustBundle, orgAssignmentEnvelope, parseOrgManifest, signCanonical, signEnvelope, subagentAddress, validateOrgManifest, verifyComprehensionAttestation } from "@sentropic/h2a";
45
46
  import { H2A_CLAUDE_HOST } from "./hosts/claude.js";
46
47
  import { H2A_CODEX_HOST } from "./hosts/codex.js";
47
48
  import { H2A_GEMINI_HOST } from "./hosts/gemini.js";
48
49
  import { H2A_AGY_HOST } from "./hosts/agy.js";
49
50
  import { H2A_CLI_MCP_TOOL_NAMES } from "./mcp.js";
50
- import { renderStopHook, claudeStopHookEntry, isH2ARecordHook, codexPluginManifest, codexMarketplaceManifest, codexPluginTrustCommands, H2A_CODEX_PLUGIN_NAME, H2A_HOST_PLUGIN_HOSTS } from "./hosts/plugin.js";
51
- import { H2A_STORE_SCHEMA_VERSION, createLocalStore, listPresence, sanitizeStorePaths } from "./runtime/local-files/index.js";
51
+ import { renderStopHook, claudeStopHookEntry, claudeDriveReceiveHookEntry, isH2ADriveReceiveHook, isH2ARecordHook, codexPluginManifest, codexMarketplaceManifest, codexPluginTrustCommands, H2A_CODEX_PLUGIN_NAME, H2A_HOST_PLUGIN_HOSTS } from "./hosts/plugin.js";
52
+ import { H2A_STORE_SCHEMA_VERSION, createLocalStore, listPresence, safePathSegment, sanitizeStorePaths } from "./runtime/local-files/index.js";
52
53
  import { runMcpStdio } from "./runtime/mcp/index.js";
53
54
  import { renderK8sSidecar } from "./runtime/deploy/k8s-sidecar.js";
54
55
  import { renderK8sTenant } from "./runtime/deploy/k8s-tenant.js";
@@ -57,7 +58,7 @@ import { recordStop, scanDrumbeat, clearDrumbeatEntry, runDrumbeatWatch as runDr
57
58
  import { gatherNhiSnapshot } from "./runtime/nhi.js";
58
59
  import { raiseBlockage, listBlockages, resolveBlockage } from "./runtime/blockage/index.js";
59
60
  import { recordEscalation, listEscalations, clearEscalation } from "./runtime/escalation/index.js";
60
- import { authorizeDrive, chainDriver, formatSignedDriveInstruction, headlessDriver, localTmuxDriver, loggingDriver, nativeBackchannelDriver } from "./runtime/drive/index.js";
61
+ import { authorizeDrive, chainDriver, formatSignedDriveInstruction, headlessDriver, localTmuxDriver, loggingDriver, nativeBackchannelDriver, remoteDriveServerForStore, verifyDriveOnReceive } from "./runtime/drive/index.js";
61
62
  import { verifyEnvelopeSysmlRef } from "./runtime/sysml/index.js";
62
63
  import { checkUpgrade, performUpgrade, currentCliVersion, upgradeCachePath, canReexec, reexecSelf, H2A_REEXEC_GUARD_ENV } from "./runtime/upgrade/index.js";
63
64
  import { resolveLiveIdentity } from "./runtime/identity/index.js";
@@ -132,6 +133,8 @@ export function renderCliHelp() {
132
133
  " h2a negotiate journal --id <id> [--root <path>]",
133
134
  " h2a declare-interest --negotiation <id> --instance <id> --interets <a,b> [--bindings <scope,...>] [--masque-impact-collectif] [--event-id <id>] [--root <path>]",
134
135
  " h2a conflict-posture --negotiation <id> [--root <path>]",
136
+ " h2a dossier --negotiation <id> [--presenter <id>] [--advisory-gate] [--event-id <id>] [--root <path>]",
137
+ " h2a confiance --negotiation <id> [--root <path>]",
135
138
  " h2a attest-comprehension --instance <id> --dossier <file|sha256:...> --private-key <pem-path> [--negotiation <id> | --to <instance>] [--role <role>] [--scope <scope>] [--root <path>]",
136
139
  " h2a comprehension list --negotiation <id> [--root <path>]",
137
140
  " h2a comprehension verify --json <event-or-envelope-json> --public-key <pem-file>",
@@ -150,7 +153,9 @@ export function renderCliHelp() {
150
153
  " h2a upgrade [--check] (--check: report current vs latest; bare: npm i -g @sentropic/h2a-cli@latest)",
151
154
  " h2a remote serve [--port <n>] [--host <h>] [--path </h2a/envelopes>] [--root <path>]",
152
155
  " h2a remote send --url <u> --instance <signer> --private-key <pem> --json <envelope>",
153
- " h2a drive --from <instance> --to <instance> --instruction <text> --private-key <pem> [--driver logging|native|local-tmux|headless|auto] [--root <path>]",
156
+ " h2a drive --from <instance> --to <instance> --instruction <text> --private-key <pem> [--driver logging|native|local-tmux|headless|auto] [--host <host>] [--root <path>]",
157
+ " h2a drive receive --to <instance> (--line <signed-line> | --stdin) [--ignore-non-drive] [--root <path>] (verify-before-act gate for host hooks)",
158
+ " h2a drive serve --to <instance> --inject-command <command> [--port <n>] [--host <h>] [--path </h2a/drive>] [--root <path>] (remote verify-before-inject service)",
154
159
  " h2a drumbeat record --instance <id> --status <working|paused|done|blocked|out-of-tokens> [--command <c>] [--resume-command <c>] [--tmux-session <s> --tmux-pane <p>] [--root <path>]",
155
160
  " h2a drumbeat scan [--max-relances <n>] [--root <path>]",
156
161
  " h2a drumbeat clear --instance <id> [--root <path>]",
@@ -776,6 +781,48 @@ function cmdConflictPosture(flags, streams) {
776
781
  return classifyStoreError(message);
777
782
  }
778
783
  }
784
+ function cmdDossier(flags, streams) {
785
+ if (!flags.negotiation) {
786
+ streams.stderr.write("h2a dossier: --negotiation <id> required\n");
787
+ return 1;
788
+ }
789
+ const cwd = streams.cwd ?? (() => process.cwd());
790
+ const root = resolveRoot(flags, cwd);
791
+ const store = createLocalStore({ root });
792
+ try {
793
+ const result = store.deriveDecisionDossier(flags.negotiation, {
794
+ presenter: flags.presenter,
795
+ advisoryGate: flags["advisory-gate"] === "true",
796
+ eventId: flags["event-id"]
797
+ });
798
+ streams.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
799
+ return 0;
800
+ }
801
+ catch (error) {
802
+ const message = error.message;
803
+ streams.stderr.write(`h2a dossier: ${message}\n`);
804
+ return classifyStoreError(message);
805
+ }
806
+ }
807
+ function cmdConfiance(flags, streams) {
808
+ if (!flags.negotiation) {
809
+ streams.stderr.write("h2a confiance: --negotiation <id> required\n");
810
+ return 1;
811
+ }
812
+ const cwd = streams.cwd ?? (() => process.cwd());
813
+ const root = resolveRoot(flags, cwd);
814
+ const store = createLocalStore({ root });
815
+ try {
816
+ const posture = store.derivePostureConfiance(flags.negotiation);
817
+ streams.stdout.write(`${JSON.stringify({ negotiationId: flags.negotiation, posture }, null, 2)}\n`);
818
+ return 0;
819
+ }
820
+ catch (error) {
821
+ const message = error.message;
822
+ streams.stderr.write(`h2a confiance: ${message}\n`);
823
+ return classifyStoreError(message);
824
+ }
825
+ }
779
826
  function cmdAttestComprehension(flags, streams) {
780
827
  if (!flags.instance || !flags.dossier || !flags["private-key"]) {
781
828
  streams.stderr.write("h2a attest-comprehension: --instance <id> --dossier <file|sha256:...> --private-key <pem-path> required\n");
@@ -1109,6 +1156,82 @@ export async function runRemoteServe(flags, io = { stdout: process.stdout, stder
1109
1156
  server.on("close", () => resolve(0));
1110
1157
  });
1111
1158
  }
1159
+ function commandDriveInjector(command, io) {
1160
+ return (payload, signedLine) => {
1161
+ const result = spawnSync(command, {
1162
+ shell: true,
1163
+ input: `${signedLine}\n`,
1164
+ encoding: "utf8",
1165
+ env: {
1166
+ ...process.env,
1167
+ H2A_DRIVE_LINE: signedLine,
1168
+ H2A_DRIVE_FROM: payload.from,
1169
+ H2A_DRIVE_TO: payload.to,
1170
+ H2A_DRIVE_INSTRUCTION: payload.instruction
1171
+ },
1172
+ timeout: 30_000
1173
+ });
1174
+ if (result.error) {
1175
+ io.stderr.write(`h2a drive serve: injector command failed: ${result.error.message}\n`);
1176
+ return false;
1177
+ }
1178
+ if (result.status !== 0) {
1179
+ io.stderr.write(`h2a drive serve: injector command exited ${result.status ?? "unknown"}\n`);
1180
+ return false;
1181
+ }
1182
+ return true;
1183
+ };
1184
+ }
1185
+ /**
1186
+ * `h2a drive serve` (EVO-1 E1d): long-running HTTP endpoint for remote/sidecar
1187
+ * injection. It verifies signature, target, authority, freshness, and replay
1188
+ * before crossing the remote trust boundary into the caller-provided injector.
1189
+ */
1190
+ export async function runDriveServe(flags, io = { stdout: process.stdout, stderr: process.stderr }, options = {}) {
1191
+ if (!flags.to) {
1192
+ io.stderr.write("h2a drive serve: --to is required\n");
1193
+ return 1;
1194
+ }
1195
+ const inject = options.inject ?? (flags["inject-command"] ? commandDriveInjector(flags["inject-command"], io) : undefined);
1196
+ if (!inject) {
1197
+ io.stderr.write("h2a drive serve: --inject-command is required\n");
1198
+ return 1;
1199
+ }
1200
+ const cwd = io.cwd ?? (() => process.cwd());
1201
+ const root = resolveRoot(flags, cwd);
1202
+ const host = flags.host ?? "127.0.0.1";
1203
+ const port = flags.port ? Number.parseInt(flags.port, 10) : 8788;
1204
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
1205
+ io.stderr.write(`h2a drive serve: invalid --port "${flags.port}"\n`);
1206
+ return 1;
1207
+ }
1208
+ let store;
1209
+ try {
1210
+ store = createLocalStore({ root });
1211
+ }
1212
+ catch (error) {
1213
+ io.stderr.write(`h2a drive serve: ${error.message}\n`);
1214
+ return 1;
1215
+ }
1216
+ const server = remoteDriveServerForStore(store, {
1217
+ to: flags.to,
1218
+ path: flags.path,
1219
+ inject
1220
+ });
1221
+ return await new Promise((resolve) => {
1222
+ server.on("error", (err) => {
1223
+ io.stderr.write(`h2a drive serve: ${err.message}\n`);
1224
+ resolve(1);
1225
+ });
1226
+ server.listen(port, host, () => {
1227
+ const addr = server.address();
1228
+ const boundPort = typeof addr === "object" && addr ? addr.port : port;
1229
+ io.stdout.write(`h2a drive serve: listening on http://${host}:${boundPort}${flags.path ?? "/h2a/drive"} (to ${flags.to}, root ${root})\n`);
1230
+ io.onListening?.(server);
1231
+ });
1232
+ server.on("close", () => resolve(0));
1233
+ });
1234
+ }
1112
1235
  /**
1113
1236
  * `h2a remote send` (DEC-077): sign an envelope and POST it to a remote h2a
1114
1237
  * endpoint. Async (network), so dispatched from bin.ts. Exit 0 on a 2xx,
@@ -1703,6 +1826,38 @@ function buildDriveDriver(kind, log) {
1703
1826
  };
1704
1827
  }
1705
1828
  }
1829
+ function driveReplayGuard(root) {
1830
+ const dir = join(root, "drive-replay");
1831
+ return {
1832
+ accept(envelope, now = Date.now()) {
1833
+ const freshness = checkEnvelopeFreshness(envelope, { now });
1834
+ if (!freshness.ok)
1835
+ return freshness;
1836
+ mkdirSync(dir, { recursive: true });
1837
+ const file = join(dir, `${safePathSegment(envelope.id)}.json`);
1838
+ try {
1839
+ writeFileSync(file, `${JSON.stringify({ id: envelope.id, createdAt: envelope.createdAt })}\n`, { encoding: "utf8", flag: "wx" });
1840
+ }
1841
+ catch (error) {
1842
+ if (error.code === "EEXIST") {
1843
+ return { ok: false, reason: "replayed" };
1844
+ }
1845
+ throw error;
1846
+ }
1847
+ return { ok: true };
1848
+ },
1849
+ size() {
1850
+ try {
1851
+ return readdirSync(dir).filter((entry) => entry.endsWith(".json")).length;
1852
+ }
1853
+ catch (error) {
1854
+ if (error.code === "ENOENT")
1855
+ return 0;
1856
+ throw error;
1857
+ }
1858
+ }
1859
+ };
1860
+ }
1706
1861
  function cmdDrive(flags, streams) {
1707
1862
  const cwd = streams.cwd ?? (() => process.cwd());
1708
1863
  if (!flags.from || !flags.to || !flags.instruction || !flags["private-key"]) {
@@ -1738,6 +1893,7 @@ function cmdDrive(flags, streams) {
1738
1893
  const driver = buildDriveDriver(kind, log);
1739
1894
  const result = driver.drive({
1740
1895
  to: flags.to,
1896
+ host: flags.host ?? flags.to.split(":", 1)[0],
1741
1897
  instructionLine,
1742
1898
  launchContext: latestLaunchContext(root, flags.to)
1743
1899
  });
@@ -1756,6 +1912,59 @@ function cmdDrive(flags, streams) {
1756
1912
  }, null, 2)}\n`);
1757
1913
  return driven ? 0 : 2;
1758
1914
  }
1915
+ function stdinText(streams) {
1916
+ if (typeof streams.stdinText === "function")
1917
+ return streams.stdinText();
1918
+ return streams.stdinText ?? readFileSync(0, "utf8");
1919
+ }
1920
+ function driveLineFromStdin(raw) {
1921
+ return driveLineFromHookInput(raw) ?? "";
1922
+ }
1923
+ function cmdDriveReceive(flags, streams) {
1924
+ const cwd = streams.cwd ?? (() => process.cwd());
1925
+ const line = flags.line ?? (flags.stdin ? driveLineFromStdin(stdinText(streams) ?? "") : undefined);
1926
+ if (!flags.to) {
1927
+ streams.stderr.write("h2a drive receive: --to is required\n");
1928
+ return 1;
1929
+ }
1930
+ if (!line) {
1931
+ if (flags["ignore-non-drive"]) {
1932
+ streams.stdout.write(`${JSON.stringify({ ok: true, ignored: true, reason: "non-drive" }, null, 2)}\n`);
1933
+ return 0;
1934
+ }
1935
+ streams.stderr.write("h2a drive receive: --line or --stdin is required\n");
1936
+ return 1;
1937
+ }
1938
+ if (flags["ignore-non-drive"] && !line.trim().startsWith("[h2a ")) {
1939
+ streams.stdout.write(`${JSON.stringify({ ok: true, ignored: true, reason: "non-drive" }, null, 2)}\n`);
1940
+ return 0;
1941
+ }
1942
+ const root = resolveRoot(flags, cwd);
1943
+ const store = createLocalStore({ root });
1944
+ const now = flags.now ? Number.parseInt(flags.now, 10) : undefined;
1945
+ if (flags.now && Number.isNaN(now)) {
1946
+ streams.stderr.write(`h2a drive receive: --now must be a millisecond timestamp (got "${flags.now}")\n`);
1947
+ return 1;
1948
+ }
1949
+ let result;
1950
+ try {
1951
+ result = verifyDriveOnReceive(store, line, {
1952
+ to: flags.to,
1953
+ guard: driveReplayGuard(root),
1954
+ ...(now !== undefined ? { now } : {})
1955
+ });
1956
+ }
1957
+ catch (error) {
1958
+ streams.stderr.write(`h2a drive receive: ${error.message}\n`);
1959
+ return 3;
1960
+ }
1961
+ if (!result.ok) {
1962
+ streams.stderr.write(`h2a drive receive: ${result.reason}\n`);
1963
+ return 2;
1964
+ }
1965
+ streams.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1966
+ return 0;
1967
+ }
1759
1968
  export async function runDrumbeatRelanceInbox(flags, io = {
1760
1969
  stdout: process.stdout,
1761
1970
  stderr: process.stderr
@@ -1964,6 +2173,50 @@ function isPlainObject(value) {
1964
2173
  function configsEqual(a, b) {
1965
2174
  return JSON.stringify(a) === JSON.stringify(b);
1966
2175
  }
2176
+ function driveLineFromText(text) {
2177
+ const matchingLine = text
2178
+ .split(/\r?\n/)
2179
+ .map((line) => line.trim())
2180
+ .find((line) => line.startsWith("[h2a "));
2181
+ return matchingLine ?? (text.trim() ? text.trim() : undefined);
2182
+ }
2183
+ function hookPromptText(value) {
2184
+ if (typeof value === "string")
2185
+ return value;
2186
+ if (Array.isArray(value)) {
2187
+ for (const item of value) {
2188
+ const text = hookPromptText(item);
2189
+ if (text)
2190
+ return text;
2191
+ }
2192
+ return undefined;
2193
+ }
2194
+ if (!isPlainObject(value))
2195
+ return undefined;
2196
+ for (const key of ["prompt", "line", "message", "text", "input"]) {
2197
+ const text = hookPromptText(value[key]);
2198
+ if (text)
2199
+ return text;
2200
+ }
2201
+ if (isPlainObject(value.params)) {
2202
+ const text = hookPromptText(value.params);
2203
+ if (text)
2204
+ return text;
2205
+ }
2206
+ return undefined;
2207
+ }
2208
+ function driveLineFromHookInput(raw) {
2209
+ try {
2210
+ const parsed = JSON.parse(raw);
2211
+ const promptText = hookPromptText(parsed);
2212
+ if (promptText)
2213
+ return driveLineFromText(promptText);
2214
+ }
2215
+ catch {
2216
+ // Non-JSON hook payloads may already be the prompt text.
2217
+ }
2218
+ return driveLineFromText(raw);
2219
+ }
1967
2220
  function cmdHostSetup(flags, streams) {
1968
2221
  const host = flags.host;
1969
2222
  if (!host) {
@@ -2139,7 +2392,7 @@ function cmdHostPlugin(flags, streams) {
2139
2392
  const manifestPath = join(pluginDir, ".codex-plugin", "plugin.json");
2140
2393
  const marketplacePath = join(marketplaceDir, ".agents", "plugins", "marketplace.json");
2141
2394
  // The hooks.json is the same idempotent Claude-format merge as --write.
2142
- const hooksMerge = mergeStopHooksFile(hooksPath, render.record, flags.force === "true", streams);
2395
+ const hooksMerge = mergeStopHooksFile(hooksPath, render.record, render.receive, flags.force === "true", streams);
2143
2396
  if (typeof hooksMerge === "number")
2144
2397
  return hooksMerge;
2145
2398
  // Manifests are stable identities — rewriting them is idempotent (same bytes).
@@ -2167,7 +2420,7 @@ function cmdHostPlugin(flags, streams) {
2167
2420
  manifest: manifestPath,
2168
2421
  hooks: hooksPath,
2169
2422
  mechanism: render.mechanism,
2170
- hook: "hooks.Stop",
2423
+ hook: "hooks.Stop + hooks.UserPromptSubmit",
2171
2424
  // The trust step is surfaced, not silently dropped: codex cannot be
2172
2425
  // auto-trusted from outside, so run these to load the plugin.
2173
2426
  trust: codexPluginTrustCommands(marketplaceDir, H2A_CODEX_PLUGIN_NAME)
@@ -2187,7 +2440,7 @@ function cmdHostPlugin(flags, streams) {
2187
2440
  return 1;
2188
2441
  }
2189
2442
  const targetPath = flags.write;
2190
- const merge = mergeStopHooksFile(targetPath, render.record, flags.force === "true", streams);
2443
+ const merge = mergeStopHooksFile(targetPath, render.record, render.receive, flags.force === "true", streams);
2191
2444
  if (typeof merge === "number")
2192
2445
  return merge;
2193
2446
  streams.stdout.write(`${JSON.stringify({
@@ -2195,7 +2448,7 @@ function cmdHostPlugin(flags, streams) {
2195
2448
  host: flags.host,
2196
2449
  written: targetPath,
2197
2450
  mechanism: render.mechanism,
2198
- hook: "hooks.Stop",
2451
+ hook: "hooks.Stop + hooks.UserPromptSubmit",
2199
2452
  // codex loads hooks via a plugin (manifest → ./hooks/hooks.json); the
2200
2453
  // file written above is a valid codex hooks.json, but codex still
2201
2454
  // needs the manifest + trust. `--scaffold <dir>` does the full job;
@@ -2214,13 +2467,14 @@ function cmdHostPlugin(flags, streams) {
2214
2467
  return 0;
2215
2468
  }
2216
2469
  /**
2217
- * Idempotently merge the h2a drumbeat-record `Stop` hook into a Claude-format
2218
- * hooks file (claude/gemini `settings.json` or a codex `hooks.json`). Drops any
2219
- * prior h2a Stop hook, appends the freshly-rendered one, preserves unrelated
2220
- * keys/hooks. Returns an exit code on failure, or `undefined` on success.
2470
+ * Idempotently merge the h2a drumbeat-record `Stop` hook and the drive receive
2471
+ * `UserPromptSubmit` gate into a Claude-format hooks file (claude/gemini
2472
+ * `settings.json` or a codex `hooks.json`). Drops prior h2a hooks, appends the
2473
+ * freshly-rendered ones, and preserves unrelated keys/hooks. Returns an exit
2474
+ * code on failure, or `undefined` on success.
2221
2475
  * DEC-102/103/104; reused by `--scaffold` (DEC-113).
2222
2476
  */
2223
- function mergeStopHooksFile(targetPath, record, force, streams) {
2477
+ function mergeStopHooksFile(targetPath, record, receive, force, streams) {
2224
2478
  let existing = {};
2225
2479
  if (existsSync(targetPath)) {
2226
2480
  let raw;
@@ -2250,12 +2504,24 @@ function mergeStopHooksFile(targetPath, record, force, streams) {
2250
2504
  }
2251
2505
  const existingHooks = isPlainObject(existing.hooks) ? existing.hooks : {};
2252
2506
  const existingStop = Array.isArray(existingHooks.Stop) ? [...existingHooks.Stop] : [];
2507
+ const existingUserPromptSubmit = Array.isArray(existingHooks.UserPromptSubmit)
2508
+ ? [...existingHooks.UserPromptSubmit]
2509
+ : [];
2253
2510
  // Idempotent: drop any prior h2a drumbeat-record Stop hook, then append ours.
2254
2511
  const withoutH2A = existingStop.filter((e) => !isH2ARecordHook(e));
2255
2512
  const mergedStop = [...withoutH2A, claudeStopHookEntry(record)];
2513
+ const withoutDriveReceive = existingUserPromptSubmit.filter((e) => !isH2ADriveReceiveHook(e));
2514
+ const mergedUserPromptSubmit = [
2515
+ ...withoutDriveReceive,
2516
+ claudeDriveReceiveHookEntry(receive)
2517
+ ];
2256
2518
  const merged = {
2257
2519
  ...existing,
2258
- hooks: { ...existingHooks, Stop: mergedStop }
2520
+ hooks: {
2521
+ ...existingHooks,
2522
+ Stop: mergedStop,
2523
+ UserPromptSubmit: mergedUserPromptSubmit
2524
+ }
2259
2525
  };
2260
2526
  try {
2261
2527
  const dir = dirname(targetPath);
@@ -3022,6 +3288,10 @@ export function runCli(argv = process.argv.slice(2), streams = {
3022
3288
  return cmdDeclareInteret(flags, streams);
3023
3289
  if (command === "conflict-posture")
3024
3290
  return cmdConflictPosture(flags, streams);
3291
+ if (command === "dossier")
3292
+ return cmdDossier(flags, streams);
3293
+ if (command === "confiance")
3294
+ return cmdConfiance(flags, streams);
3025
3295
  if (command === "attest-comprehension")
3026
3296
  return cmdAttestComprehension(flags, streams);
3027
3297
  if (command === "comprehension")
@@ -3044,8 +3314,16 @@ export function runCli(argv = process.argv.slice(2), streams = {
3044
3314
  return cmdInstallSkills(flags, streams);
3045
3315
  if (command === "deploy")
3046
3316
  return cmdDeploy(argv.slice(1), streams);
3047
- if (command === "drive")
3317
+ if (command === "drive") {
3318
+ if (argv[1] === "receive") {
3319
+ return cmdDriveReceive(parseFlags(argv.slice(1)).flags, streams);
3320
+ }
3321
+ if (argv[1] === "serve") {
3322
+ streams.stderr.write("h2a drive serve: async verb — run via the h2a binary, not the synchronous API.\n");
3323
+ return 1;
3324
+ }
3048
3325
  return cmdDrive(flags, streams);
3326
+ }
3049
3327
  if (command === "remote") {
3050
3328
  // `remote serve`/`remote send` are async and dispatched from bin.ts; if we
3051
3329
  // reach here it is a misuse (e.g. via the sync runCli) or an unknown sub.