@sentropic/h2a-cli 0.25.1 → 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.
- package/dist/bin.js +4 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/cli-contract.js +33 -1
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +294 -16
- package/dist/cli.js.map +1 -1
- package/dist/hosts/plugin.d.ts +14 -2
- package/dist/hosts/plugin.d.ts.map +1 -1
- package/dist/hosts/plugin.js +23 -0
- package/dist/hosts/plugin.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/drive/index.d.ts +84 -0
- package/dist/runtime/drive/index.d.ts.map +1 -1
- package/dist/runtime/drive/index.js +325 -22
- package/dist/runtime/drive/index.js.map +1 -1
- package/dist/runtime/drumbeat/relaunchers.d.ts +5 -1
- package/dist/runtime/drumbeat/relaunchers.d.ts.map +1 -1
- package/dist/runtime/drumbeat/relaunchers.js +12 -6
- package/dist/runtime/drumbeat/relaunchers.js.map +1 -1
- package/dist/runtime/local-files/store.d.ts +14 -1
- package/dist/runtime/local-files/store.d.ts.map +1 -1
- package/dist/runtime/local-files/store.js +141 -1
- package/dist/runtime/local-files/store.js.map +1 -1
- package/dist/runtime/mcp-http/app.d.ts.map +1 -1
- package/dist/runtime/mcp-http/app.js +4 -1
- package/dist/runtime/mcp-http/app.js.map +1 -1
- package/package.json +2 -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
|
|
2218
|
-
*
|
|
2219
|
-
*
|
|
2220
|
-
* keys/hooks. Returns an exit
|
|
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: {
|
|
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.
|