@nihaopaul/opencode-agent-controlplane 0.0.0-stage → 0.1.7

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 (3) hide show
  1. package/README.md +76 -2
  2. package/index.js +127 -0
  3. package/package.json +26 -4
package/README.md CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
1
+ # Agent Control Plane OpenCode plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Connects a local OpenCode instance to an Agent Control Plane without a second
4
+ tunnel implementation. The plugin invokes the released `node-tunnel` helper in
5
+ `connect` mode. It does **not** restart OpenCode or manage the local OpenCode
6
+ version.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ opencode plugin add @nihaopaul/opencode-agent-controlplane@0.1.7
12
+ ```
13
+
14
+ Point it at a short enrol link with either an environment variable:
15
+
16
+ ```sh
17
+ export AGENT_CONTROLPLANE_ENROLL_URL='https://control.example.com/_/enroll/7K3M9Q2B'
18
+ opencode
19
+ ```
20
+
21
+ or an OpenCode plugin entry:
22
+
23
+ ```json
24
+ {
25
+ "plugin": [
26
+ [
27
+ "@nihaopaul/opencode-agent-controlplane@0.1.7",
28
+ { "enroll_url": "https://control.example.com/_/enroll/7K3M9Q2B" }
29
+ ]
30
+ ]
31
+ }
32
+ ```
33
+
34
+ The helper registers, waits for approval in the control plane, stores its secret
35
+ under `$XDG_STATE_HOME/opencode`, enrols a certificate, and runs the tunnel. It
36
+ does not start, stop, upgrade, or restart OpenCode.
37
+
38
+ Options:
39
+
40
+ | Option | Environment | Default |
41
+ | --- | --- | --- |
42
+ | `enroll_url` | `AGENT_CONTROLPLANE_ENROLL_URL` | required |
43
+ | `node_tunnel` | `AGENT_CONTROLPLANE_NODE_TUNNEL` | `node-tunnel` on `PATH` |
44
+ | - | `AGENT_CONTROLPLANE_LOG` | `$XDG_STATE_HOME/opencode/agent-controlplane.log` |
45
+
46
+ Plain HTTP enrol URLs are accepted only for loopback. The helper log is created
47
+ with mode `0600`.
48
+
49
+ ## Requirements
50
+
51
+ - the released `node-tunnel` binary on `PATH`, or an explicit `node_tunnel`;
52
+ - Node.js 22.14 or newer;
53
+ - outbound access to the control plane, directly or through the standard proxy
54
+ variables supported by `node-tunnel`.
55
+
56
+ ## Publishing
57
+
58
+ This package is designed to publish with **npm trusted publishing (OIDC)** and
59
+ no long-lived `NPM_TOKEN`.
60
+
61
+ On npmjs.com, open the package settings and add a trusted publisher:
62
+
63
+ - **Organization or user:** `nihaopaul`
64
+ - **Repository:** `agent-controlplane`
65
+ - **Workflow filename:** `publish-plugin.yml`
66
+ - **Allowed actions:** `npm publish`
67
+ - **Environment name:** optional; leave empty unless you add one to the workflow
68
+
69
+ Then push a `plugin-v*` tag. `.github/workflows/publish-plugin.yml` uses
70
+ `id-token: write` and runs `npm publish --access public` without a token.
71
+
72
+ The repository is private, so npm cannot generate provenance attestations even
73
+ with trusted publishing. Authentication is still OIDC-bound to this repository
74
+ and workflow. If provenance is required, publish from a public source mirror.
75
+
76
+ The package name is `@nihaopaul/opencode-agent-controlplane`; change the name in
77
+ `package.json`, the workflow, and the trusted-publisher configuration together.
package/index.js ADDED
@@ -0,0 +1,127 @@
1
+ import { spawn } from "node:child_process";
2
+ import { chmodSync, closeSync, mkdirSync, openSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+
6
+ const DEFAULT_BINARY = "node-tunnel";
7
+ const ENV_URL = "AGENT_CONTROLPLANE_ENROLL_URL";
8
+ const ENV_BINARY = "AGENT_CONTROLPLANE_NODE_TUNNEL";
9
+ const ENV_LOG = "AGENT_CONTROLPLANE_LOG";
10
+ const LOOPBACK = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
11
+ const ENROLL_PATH = /^\/_\/(?:enroll\/[A-Za-z0-9-]{1,64}|connect)$/;
12
+
13
+ /**
14
+ * Validate a short enrol link (or the legacy connect link) before it is passed
15
+ * to the helper. Plain HTTP is only accepted for loopback development.
16
+ */
17
+ export function validateEnrollUrl(value) {
18
+ if (typeof value !== "string" || !value.trim()) {
19
+ throw new Error("enrol URL is required");
20
+ }
21
+ let url;
22
+ try {
23
+ url = new URL(value.trim());
24
+ } catch {
25
+ throw new Error("enrol URL is not a valid URL");
26
+ }
27
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
28
+ throw new Error("enrol URL must use https://");
29
+ }
30
+ if (url.protocol !== "https:" && !LOOPBACK.has(url.hostname)) {
31
+ throw new Error("plain HTTP enrol URLs are only allowed for loopback");
32
+ }
33
+ if (url.username || url.password) {
34
+ throw new Error("enrol URL must not contain credentials");
35
+ }
36
+ if (!ENROLL_PATH.test(url.pathname)) {
37
+ throw new Error("enrol URL must be /_/enroll/<code> or /_/connect");
38
+ }
39
+ return url.toString();
40
+ }
41
+
42
+ export function enrollUrlFrom(options = {}, env = process.env) {
43
+ const value = options?.enroll_url ?? options?.enrollUrl ?? env[ENV_URL];
44
+ return value ? validateEnrollUrl(value) : null;
45
+ }
46
+
47
+ export function logPath(env = process.env) {
48
+ if (env[ENV_LOG]) return env[ENV_LOG];
49
+ const stateHome = env.XDG_STATE_HOME || join(env.HOME || homedir(), ".local", "state");
50
+ return join(stateHome, "opencode", "agent-controlplane.log");
51
+ }
52
+
53
+ function binaryFrom(options = {}, env = process.env) {
54
+ const configured = options?.node_tunnel ?? options?.binary ?? env[ENV_BINARY];
55
+ return typeof configured === "string" && configured.trim() ? configured.trim() : DEFAULT_BINARY;
56
+ }
57
+
58
+ /**
59
+ * Start `node-tunnel connect <url>` in the background. The helper owns
60
+ * registration, approval, secret storage, enrolment, and the tunnel. The plugin
61
+ * never restarts OpenCode and never manages the local OpenCode version.
62
+ */
63
+ export function startHelper({
64
+ url,
65
+ binary = DEFAULT_BINARY,
66
+ logFile = logPath(),
67
+ env = process.env,
68
+ spawnImpl = spawn,
69
+ } = {}) {
70
+ mkdirSync(dirname(logFile), { recursive: true, mode: 0o700 });
71
+ const fd = openSync(logFile, "a", 0o600);
72
+ let child;
73
+ try {
74
+ try {
75
+ chmodSync(logFile, 0o600);
76
+ } catch {
77
+ // The file may be on a filesystem without POSIX modes.
78
+ }
79
+ child = spawnImpl(binary, ["connect", url], {
80
+ detached: false,
81
+ stdio: ["ignore", fd, fd],
82
+ env: { ...env, RUST_LOG: env.RUST_LOG || "info" },
83
+ windowsHide: true,
84
+ });
85
+ } finally {
86
+ closeSync(fd);
87
+ }
88
+ child.on?.("error", (error) => {
89
+ console.error(`[agent-controlplane] failed to start ${binary}: ${error.message}`);
90
+ });
91
+ return child;
92
+ }
93
+
94
+ export function stopHelper(child) {
95
+ if (!child || child.killed) return;
96
+ try {
97
+ child.kill("SIGTERM");
98
+ } catch {
99
+ // The process may already be gone.
100
+ }
101
+ }
102
+
103
+ let activeChild = null;
104
+
105
+ export const AgentControlPlane = async (_input, options = {}) => {
106
+ try {
107
+ const url = enrollUrlFrom(options);
108
+ if (!url) return {};
109
+ if (activeChild && !activeChild.killed) {
110
+ console.error("[agent-controlplane] helper already running; leaving it in place");
111
+ return {};
112
+ }
113
+ const child = startHelper({ url, binary: binaryFrom(options) });
114
+ activeChild = child;
115
+ return {
116
+ dispose: async () => {
117
+ if (activeChild === child) activeChild = null;
118
+ stopHelper(child);
119
+ },
120
+ };
121
+ } catch (error) {
122
+ console.error(`[agent-controlplane] ${error instanceof Error ? error.message : String(error)}`);
123
+ return {};
124
+ }
125
+ };
126
+
127
+ export default AgentControlPlane;
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "@nihaopaul/opencode-agent-controlplane",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.7",
4
+ "description": "Connect a local OpenCode instance to an Agent Control Plane by invoking the released node-tunnel helper.",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": "./index.js",
8
+ "files": [
9
+ "index.js",
10
+ "README.md"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/nihaopaul/agent-controlplane.git",
15
+ "directory": "plugin"
16
+ },
17
+ "license": "UNLICENSED",
18
+ "engines": {
19
+ "node": ">=22.14"
20
+ },
21
+ "scripts": {
22
+ "test": "node --test test/*.test.js",
23
+ "pack:check": "npm pack --dry-run"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ }
28
+ }