@securecoms/agent-bridge 0.6.0 → 0.8.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 (30) hide show
  1. package/README.md +2 -0
  2. package/connectors/hermes/__init__.py +60 -0
  3. package/connectors/hermes/__pycache__/__init__.cpython-311.pyc +0 -0
  4. package/connectors/hermes/__pycache__/adapter.cpython-311.pyc +0 -0
  5. package/connectors/hermes/__pycache__/gate.cpython-311.pyc +0 -0
  6. package/connectors/hermes/__pycache__/securecoms_gate.cpython-311.pyc +0 -0
  7. package/connectors/hermes/__pycache__/securecoms_transport.cpython-311.pyc +0 -0
  8. package/connectors/hermes/__pycache__/transport.cpython-311.pyc +0 -0
  9. package/connectors/hermes/adapter.py +470 -0
  10. package/connectors/hermes/plugin.yaml +25 -0
  11. package/connectors/hermes/pytest.ini +3 -0
  12. package/connectors/hermes/securecoms_gate.py +70 -0
  13. package/connectors/hermes/securecoms_transport.py +257 -0
  14. package/connectors/hermes/tests/__pycache__/conftest.cpython-311-pytest-8.4.0.pyc +0 -0
  15. package/connectors/hermes/tests/__pycache__/test_adapter_wiring.cpython-311-pytest-8.4.0.pyc +0 -0
  16. package/connectors/hermes/tests/__pycache__/test_gate.cpython-311-pytest-8.4.0.pyc +0 -0
  17. package/connectors/hermes/tests/__pycache__/test_securecoms_gate.cpython-311-pytest-8.4.0.pyc +0 -0
  18. package/connectors/hermes/tests/__pycache__/test_securecoms_transport.cpython-311-pytest-8.4.0.pyc +0 -0
  19. package/connectors/hermes/tests/__pycache__/test_transport.cpython-311-pytest-8.4.0.pyc +0 -0
  20. package/connectors/hermes/tests/conftest.py +23 -0
  21. package/connectors/hermes/tests/test_adapter_wiring.py +541 -0
  22. package/connectors/hermes/tests/test_securecoms_gate.py +113 -0
  23. package/connectors/hermes/tests/test_securecoms_transport.py +608 -0
  24. package/dist/bin.d.ts +2 -0
  25. package/dist/bin.js +436 -108
  26. package/dist/service/harness-wire.d.ts +22 -0
  27. package/dist/service/hermes-config.d.ts +51 -0
  28. package/dist/service/install-prep.d.ts +5 -8
  29. package/dist/service/wire.d.ts +97 -40
  30. package/package.json +1 -1
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Strict, line-based editor for `~/.hermes/config.yaml`'s `plugins.enabled`
3
+ * block sequence. Hermes only loads user plugins listed there (plugins.py's
4
+ * loader — see spec §4), so `wire --harness hermes` must add our plugin's
5
+ * entry or the copied connector never runs.
6
+ *
7
+ * This module is PURE: it takes the file's text and returns either a noop
8
+ * (already correct / refused) or the edited text. File I/O and the
9
+ * `config.yaml.bak-wire-<ts>-<pid>` backup live with the caller (wire.ts /
10
+ * harness-wire.ts, Task 6).
11
+ *
12
+ * Hardened rules (spec §4, audited against the real production file at
13
+ * `config.yaml:569-573`):
14
+ * 1. The inserted item copies the EXISTING first item's exact column —
15
+ * never `key-indent + 2`. Production shape has items at the SAME
16
+ * indent as the `enabled:` key itself (a legal, if unusual, YAML block
17
+ * sequence style); some configs indent items by +2. Both must work,
18
+ * which is why the column is READ from the first item, never computed.
19
+ * 2. Insertion lands after the LAST consecutive same-column `- ` item —
20
+ * stop scanning at the first line that isn't one (the boundary before
21
+ * a sibling key such as `disabled: []`).
22
+ * 2b. Stopping the scan is not enough on its own: the line the scan
23
+ * stopped at must be a LEGITIMATE list terminator before the insertion
24
+ * is allowed to happen there. Legitimate means one of: (a) a sibling
25
+ * key at the same column as `enabled:` itself (e.g. `disabled:`), (b)
26
+ * end-of-block via blank-line(s) then a genuine dedent below the
27
+ * items' column, or (c) end of file. Anything else that stops the
28
+ * scan — a bare `-`, a mapping-style item (`- ` followed by an
29
+ * indented `key: value` on the next line), a blank line followed by
30
+ * MORE same-column items (ambiguous: did the writer mean to end the
31
+ * list or just add a spacer?), or any other unrecognized content —
32
+ * refuses rather than insert at a spot that was never verified to be
33
+ * the list's actual end.
34
+ * 3. `plugins.disabled` is checked too: an entry listed there wins over
35
+ * `plugins.enabled` in the loader, so refuse rather than add a second,
36
+ * shadowed entry.
37
+ * 4. Any structural surprise — no `plugins:` block, an inline
38
+ * `enabled: [...]` form, an empty enabled list, mixed-indent items, a
39
+ * comment line interleaved with items (ambiguous: it could be a
40
+ * trailing comment on the item above or a leading comment on the item
41
+ * below), or a non-boundary list terminator (see 2b) — refuses with no
42
+ * write and an actionable message. Never reformats, never touches any
43
+ * other key.
44
+ */
45
+ export declare const HERMES_PLUGIN_NAME = "securecoms-platform";
46
+ export interface EnsureHermesPluginEnabledResult {
47
+ action: "noop" | "edited";
48
+ text?: string;
49
+ reason?: string;
50
+ }
51
+ export declare function ensureHermesPluginEnabled(configText: string): EnsureHermesPluginEnabledResult;
@@ -1,4 +1,5 @@
1
1
  import { type ServiceInput } from "./config.js";
2
+ import { type AutoWireDeps } from "./harness-wire.js";
2
3
  export interface PrepDeps {
3
4
  readFile: (p: string) => string;
4
5
  writeFileMode: (p: string, data: string, mode: number) => void;
@@ -37,13 +38,6 @@ export declare function verifyDaemonUp(args: {
37
38
  attempts?: number;
38
39
  delayMs?: number;
39
40
  }, deps: PrepDeps): Promise<boolean>;
40
- export declare function harnessSnippet(args: {
41
- slug: string;
42
- port: number;
43
- tokenFilePath: string;
44
- harness: string;
45
- identityId: string;
46
- }): string;
47
41
  export interface OrchestrateInstallCtx {
48
42
  credentialAbsPath: string;
49
43
  label?: string;
@@ -52,7 +46,9 @@ export interface OrchestrateInstallCtx {
52
46
  home: string;
53
47
  launchAgentsDir?: string;
54
48
  systemdDir?: string;
49
+ connectorsSrcDir: string;
55
50
  prepDeps: PrepDeps;
51
+ autoWireDeps: AutoWireDeps;
56
52
  installService: (input: ServiceInput) => {
57
53
  label: string;
58
54
  filePath: string;
@@ -65,5 +61,6 @@ export interface OrchestrateInstallCtx {
65
61
  }
66
62
  export declare function orchestrateInstall(ctx: OrchestrateInstallCtx): Promise<{
67
63
  echo: string;
68
- harnessSnippetShown: boolean;
64
+ harnessWired: boolean;
65
+ harnessError: boolean;
69
66
  }>;
@@ -1,10 +1,43 @@
1
1
  import type { AgentFleetEntry } from "./fleet.js";
2
2
  export type Harness = "openclaw" | "hermes";
3
- export interface WireDeps {
3
+ /**
4
+ * Per-harness allow-list of file extensions the copier includes when it
5
+ * installs a connector — openclaw ships a JS runtime, hermes ships Python
6
+ * (F8: a single `.js|.json` filter silently produced an EMPTY hermes install,
7
+ * since none of adapter.py/securecoms_transport.py/securecoms_gate.py/
8
+ * plugin.yaml/__init__.py matched it).
9
+ */
10
+ export declare const CONNECTOR_FILE_EXTENSIONS: Record<Harness, readonly string[]>;
11
+ /**
12
+ * Directory names the copier never descends into, checked AT THE DIRECTORY
13
+ * BRANCH before recursion (F8) — a filename-only extension filter still
14
+ * walks into `tests/` and would copy every `*.py` it finds there, since `.py`
15
+ * is itself an allow-listed hermes extension. `__pycache__` is compiled
16
+ * bytecode, never source, for either harness.
17
+ */
18
+ export declare const CONNECTOR_SKIP_DIRS: ReadonlySet<string>;
19
+ /** The config read/backup/write seams the Hermes `plugins.enabled` auto-edit
20
+ * needs (spec §4, F4). Shared between `WireDeps` (the `wire` verb) and
21
+ * `AutoWireDeps` (the `install` auto-wire path) so `enableHermesConfig`
22
+ * below is the ONE implementation both paths call — never duplicated,
23
+ * never skippable on just one of the two. Unused for the openclaw harness,
24
+ * which has no config file to edit. */
25
+ export interface HermesConfigIoDeps {
26
+ /** Returns null when the file (or its parent ~/.hermes) doesn't exist or
27
+ * isn't readable — treated as a config-edit refusal, same posture as any
28
+ * other structural surprise `ensureHermesPluginEnabled` reports. */
29
+ readHermesConfig: (path: string) => string | null;
30
+ /** Persists the ORIGINAL text under a `hermesConfigBackupPath` name,
31
+ * called BEFORE `writeHermesConfig` — only invoked when an edit is about
32
+ * to happen, never on a noop/refusal. */
33
+ backupHermesConfig: (path: string, originalText: string) => void;
34
+ writeHermesConfig: (path: string, newText: string) => void;
35
+ }
36
+ export interface WireDeps extends HermesConfigIoDeps {
4
37
  home: string;
5
38
  argv: string[];
6
39
  readFleet: () => AgentFleetEntry[];
7
- copyConnector: (srcDir: string, dstDir: string) => void;
40
+ copyConnector: (srcDir: string, dstDir: string, harness: Harness) => void;
8
41
  writeBinding: (dstDir: string, identityId: string) => void;
9
42
  removeBinding: (dstDir: string) => void;
10
43
  getHealth: (port: number) => {
@@ -12,20 +45,17 @@ export interface WireDeps {
12
45
  } | null;
13
46
  log: (s: string) => void;
14
47
  /**
15
- * Precomputed absolute path to the reference connector's source dir
16
- * (`<pkgRoot>/connectors/openclaw`). REQUIRED: the only real caller
17
- * (bin.ts's realWireDeps) always computes this via
18
- * `connectorsSrcDir(import.meta.url)` using bin.ts's OWN import.meta.url —
19
- * see that function's doc comment for why it has to be bin.ts's URL, not
20
- * wire.ts's. A fallback that read wire.ts's own import.meta.url used to
21
- * live here, but wire.ts sits one directory deeper than bin.ts
22
- * (`src/service/wire.ts` vs `src/bin.ts`), so that fallback resolved to
23
- * the wrong path — removed rather than fixed, since every real caller
24
- * already supplies this field. Unit tests must pass a value too (any
25
- * string is fine — `copyConnector` is mocked in tests and never inspects
26
- * it for the non-hermes paths that reach it).
48
+ * Resolves a harness's connector SOURCE dir on demand, keyed by harness —
49
+ * Task 6 makes BOTH openclaw and hermes real installs, so a single
50
+ * WireDeps instance must be able to wire either one. Real callers
51
+ * (bin.ts's realWireDeps) supply `(h) => connectorsSrcDir(import.meta.url,
52
+ * h)` using bin.ts's OWN import.meta.url — see the standalone
53
+ * `connectorsSrcDir` function's doc comment below for why it has to be the
54
+ * entry point's URL, not wire.ts's own. Unit tests must pass a function
55
+ * too (any string-returning stub is fine — `copyConnector` is mocked in
56
+ * tests and never inspects the path for its own sake).
27
57
  */
28
- connectorsSrcDir: string;
58
+ connectorsSrcDir: (harness: Harness) => string;
29
59
  }
30
60
  /**
31
61
  * Detects an installed harness by probing well-known home-relative dirs.
@@ -35,42 +65,69 @@ export interface WireDeps {
35
65
  */
36
66
  export declare function detectHarness(home: string, statDir: (p: string) => boolean): Harness | null;
37
67
  /**
38
- * Where the connector gets installed for a given harness. Openclaw is the
39
- * REFERENCE CONNECTOR (Task 6, connectors/openclaw/) and is fully wired.
40
- * Hermes gets a defined target path so `detectHarness`/`wire --harness hermes`
41
- * don't crash, but the Hermes CONNECTOR ITSELF is a fast-follow — only
42
- * detection + this target dir ship in this task. Do not claim a working
43
- * Hermes connector exists.
68
+ * Where the connector gets installed for a given harness. Both harnesses are
69
+ * real installs as of Task 6: openclaw at its `local-plugins` dir; hermes at
70
+ * `~/.hermes/plugins/securecoms` — the loader's actual user-plugins dir
71
+ * (verified against the production config; `local-plugins` was a Task 4/5
72
+ * placeholder value, never the real one).
44
73
  */
45
74
  export declare function connectorTargetDir(home: string, harness: Harness): string;
46
75
  /**
47
- * Resolves the reference connector's SOURCE dir at runtime. `connectors/` is
48
- * shipped SOURCE (not compiled — H2), so this must resolve from the running
49
- * file's location back to the package root, never into dist/. Callers pass
50
- * their OWN import.meta.url (bin.ts's), not wire.ts's: esbuild bundles
51
- * wire.ts's code into dist/bin.js, so only the entry point's import.meta.url
52
- * is guaranteed to sit exactly one directory below the package root in BOTH
53
- * dev (src/bin.ts) and the built artifact (dist/bin.js) — mirrors
54
- * resolve-bin.ts's loadPackageMeta (`dist/bin.js -> ../package.json`).
76
+ * Resolves a connector's SOURCE dir at runtime, keyed by harness (`connectors/
77
+ * <harness>`) — `connectors/` is shipped SOURCE (not compiled — H2), so this
78
+ * must resolve from the running file's location back to the package root,
79
+ * never into dist/. Callers pass their OWN import.meta.url (bin.ts's), not
80
+ * wire.ts's: esbuild bundles wire.ts's code into dist/bin.js, so only the
81
+ * entry point's import.meta.url is guaranteed to sit exactly one directory
82
+ * below the package root in BOTH dev (src/bin.ts) and the built artifact
83
+ * (dist/bin.js) — mirrors resolve-bin.ts's loadPackageMeta (`dist/bin.js ->
84
+ * ../package.json`).
85
+ */
86
+ export declare function connectorsSrcDir(importMetaUrl: string, harness: Harness): string;
87
+ /** Path to the Hermes gateway's `config.yaml`, whose `plugins.enabled` block
88
+ * the auto-edit targets (spec §4). v1 targets the default `~/.hermes`
89
+ * profile only — `HERMES_HOME`/secondary profiles are documented as
90
+ * unsupported by `wire` in this round (spec §4 scope limit). */
91
+ export declare function hermesConfigPath(home: string): string;
92
+ /** Backup filename for a config.yaml auto-edit: `<path>.bak-wire-<ts>-<pid>`
93
+ * (spec §4 rule 4) — unique enough per host that two `wire` runs essentially
94
+ * never collide (different timestamp, or different pid at the same ms). */
95
+ export declare function hermesConfigBackupPath(configPath: string, ts: number, pid: number): string;
96
+ /**
97
+ * F4 (load-bearing): the ONE enable helper invoked from BOTH `runWire` (the
98
+ * `wire` verb, below) and `autoWireHarness` (the `install` auto-wire path,
99
+ * harness-wire.ts) — a hermes wire that copies the connector and writes the
100
+ * binding but skips this step is the silent half-wire the spec forbids.
101
+ * Reads config.yaml, runs Task 5's pure `ensureHermesPluginEnabled`, and on
102
+ * an actual edit backs up the ORIGINAL text before writing the new text.
103
+ * Never writes anything on a noop or a refusal.
55
104
  */
56
- export declare function connectorsSrcDir(importMetaUrl: string): string;
105
+ export declare function enableHermesConfig(home: string, deps: HermesConfigIoDeps): {
106
+ ok: boolean;
107
+ message: string;
108
+ };
57
109
  /**
58
110
  * Installs the reference connector into the harness's plugin dir and drops
59
111
  * an explicit securecoms.binding.json naming the resolved agent's identity.
60
112
  * There is NO auto-discovery (audit C1): the binding is written for EVERY
61
113
  * agent, singleton included, every time `wire` runs.
62
114
  *
63
- * Hermes has no real connector yet (this round only ships detection +
64
- * `connectorTargetDir`, see that function's doc comment) — refuse rather
65
- * than install the openclaw connector's files into a Hermes plugin dir,
66
- * which would be a false "installed" success.
115
+ * Hermes (Task 6) additionally needs its `~/.hermes/config.yaml`
116
+ * `plugins.enabled` entry (F4's `enableHermesConfig`, shared with
117
+ * autoWireHarness) — Hermes only loads plugins listed there, so the copy +
118
+ * binding alone would leave the connector inert. If that edit refuses (no
119
+ * `plugins:` block, an already-disabled entry, an ambiguous list shape, or a
120
+ * missing config.yaml entirely), the copy and binding REMAIN on disk — they
121
+ * are harmless without the enable — but `wire` exits non-zero with the
122
+ * manual-edit instructions, so callers see harness-NOT-wired rather than a
123
+ * silent half-wire.
67
124
  */
68
125
  export declare function runWire(deps: WireDeps): number;
69
126
  /** Removes the explicit binding for the resolved agent. Leaves the copied
70
- * connector files in place — unwire un-names the agent, it doesn't uninstall
71
- * the plugin (that stays a harness-native operation).
72
- *
73
- * Hermes never has anything installed by `runWire` (see its hermes guard),
74
- * so there's nothing to remove — no-op cleanly rather than call
75
- * `removeBinding` against a dir that was never created. */
127
+ * connector files (and, for hermes, the config.yaml plugin entry) in place
128
+ * — unwire un-names the agent, it doesn't uninstall the plugin (that stays
129
+ * a harness-native operation). For hermes specifically: removing the
130
+ * installed files while config.yaml still lists the plugin would make
131
+ * Hermes log load errors at boot, so the copy AND the config entry both
132
+ * deliberately remain — printed as explicit manual-cleanup instructions. */
76
133
  export declare function runUnwire(deps: WireDeps): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",