@ours.network/mcp 0.16.0 → 0.17.0-nightly.10

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/README.md CHANGED
@@ -40,6 +40,47 @@ fail atomically without retrieving or writing any file.
40
40
  |---------|---------|---------|
41
41
  | `OURS_STATE_DIR` | `~/.ours` | Node identity + serialized state. Distinct per node. |
42
42
  | `OURS_BROKER_URL` | `wss://broker1.ours.network` | The ADAPT broker to connect through. Set to `ws://localhost:9000` for a local broker. |
43
+ | `OURS_SERVICE_NAME` | *(none)* | Boot-service instance name (also `serviceName` in `config.json`). See below. |
44
+
45
+ Nightly installers can associate a client application with one local daemon profile in
46
+ `~/.ours/installer-profiles.json`. Platform shims pass `--application claude-code`, `codex`, or
47
+ `hermes` only to the client commands `proxy` and `watch`; lifecycle commands reject the flag.
48
+ Explicit `OURS_CONFIG`, `OURS_PORT`, or `OURS_STATE_DIR` wins. Otherwise the association selects
49
+ the config before the historical `~/.ours/config.json` / `3050` fallback.
50
+
51
+ An associated client fails closed: the selected config must still resolve the registered port and
52
+ state directory; unauthenticated `/info` must report that state directory before the protected API
53
+ is probed; and authentication must succeed. Drift, an unreachable daemon, or a 401/403 instructs
54
+ the operator to rerun the Nightly installer—there is no silent fallback. Token precedence remains
55
+ `OURS_API_TOKEN` → selected config `apiToken` → selected state directory `daemon-token`; tokens are
56
+ never read from or written to the registry. Installed associations force client auto-start off.
57
+
58
+ ### Running more than one daemon on a host
59
+
60
+ `install-service` bakes the resolved port, broker and state directory into a single boot
61
+ definition — `ours.service` on Linux, `solutions.adaptframework.ours` on macOS. Two daemons
62
+ installed that way would write the **same** definition, so the second silently replaces the
63
+ first.
64
+
65
+ Give a daemon an instance name and it gets its own definition instead:
66
+
67
+ ```sh
68
+ OURS_CONFIG=~/.ours-tg/config.json OURS_SERVICE_NAME=tg ours-mcp install-service
69
+ # → ours-tg.service / solutions.adaptframework.ours.tg
70
+ ```
71
+
72
+ Named definitions also persist that exact `OURS_CONFIG` path. This is required because visibility,
73
+ API-token source, transcription, GC, and other config-only settings must resolve from the same file
74
+ after reboot; persisting only port/state/broker would not preserve an auth-isolated profile.
75
+
76
+ With no name — the default, and every existing deployment — the unit and label are exactly what
77
+ they always were. A name must be 1–32 characters of letters, digits, hyphen or underscore,
78
+ starting and ending with a letter or digit; anything else is **refused** rather than rewritten,
79
+ because falling back to the shared unit is the overwrite this prevents. Removing a named
80
+ instance needs the same name: `OURS_SERVICE_NAME=tg ours-mcp uninstall-service`.
81
+
82
+ An isolated daemon also needs its own `OURS_CONFIG`/`stateDir` and its own port — the API token
83
+ lives in the state directory, so two daemons must never share one.
43
84
 
44
85
  ### Voice-message transcription
45
86
 
@@ -0,0 +1,2 @@
1
+ import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
2
+ import*as A from"node:fs";import{homedir as L}from"node:os";import{isAbsolute as U,join as M,normalize as V,resolve as H}from"node:path";import{homedir as R}from"node:os";import{join as C,resolve as D}from"node:path";var F=3050;function k(t=R()){return D(C(t,".ours"))}function J(t){return typeof t=="number"&&Number.isFinite(t)?t:void 0}function j(t){return typeof t=="string"?t:void 0}function b(t,e=R()){return{port:J(t.port)??F,stateDir:D(j(t.stateDir)??k(e))}}var E=1,x=["claude-code","codex","hermes"],n=class extends Error{code;constructor(e,o="INVALID_ASSOCIATION"){super(`ours profile association: ${e}. Re-run the Nightly installer to repair this association`),this.name="AssociationError",this.code=o}};function $(t,e,o){for(let r of Object.keys(t))if(!e.includes(r))throw new n(`${o} contains unsupported field ${JSON.stringify(r)}`)}function Q(t){let e=[],o;for(let r=0;r<t.length;r+=1){let c=t[r],i;if(c==="--application"){if(t[r+1]===void 0)throw new n("--application requires a value","INVALID_APPLICATION");i=t[++r]}else if(c.startsWith("--application="))i=c.slice(14);else{e.push(c);continue}if(!x.includes(i))throw new n(`unknown application ${JSON.stringify(i)}`,"INVALID_APPLICATION");if(o&&o!==i)throw new n("only one --application value may be supplied","INVALID_APPLICATION");o=i}return{application:o,argv:e}}function X(t,e){if(t&&e!=="proxy"&&e!=="watch")throw new n(`--application is legal only for client commands proxy and watch, not ${e}`,"APPLICATION_NOT_ALLOWED")}function z(t=process.env,e=L()){return t.OURS_INSTALL_PROFILES?P(t.OURS_INSTALL_PROFILES,"OURS_INSTALL_PROFILES"):M(e,".ours","installer-profiles.json")}function P(t,e){if(typeof t!="string"||!t||t.includes("\0")||!U(t)||V(t)!==t||t==="/")throw new n(`${e} must be a normalized absolute path`,"UNSAFE_PATH");return t}function T(t,e){let o=/^[A-Za-z0-9](?:[A-Za-z0-9_-]{0,30}[A-Za-z0-9])?$/;if(!o.test(e))throw new n(`invalid profile id ${JSON.stringify(e)}`);if(!t||typeof t!="object"||Array.isArray(t))throw new n(`profile ${JSON.stringify(e)} is not an object`);let r=t;$(r,["label","host","port","configPath","stateDir","serviceName","ownership"],`profile ${e}`);let c=typeof r.label=="string"?r.label.trim():"";if(!c)throw new n(`profile ${JSON.stringify(e)} has no label`);let i=String(r.host??"").toLowerCase();if(i!=="127.0.0.1"&&i!=="localhost")throw new n(`profile ${JSON.stringify(e)} uses non-loopback host ${JSON.stringify(r.host)}`,"UNSAFE_HOST");let s=Number(r.port);if(!Number.isInteger(s)||s<1||s>65535)throw new n(`profile ${JSON.stringify(e)} has invalid port`,"INVALID_PORT");let p=r.ownership;if(!p||typeof p!="object"||Array.isArray(p))throw new n(`profile ${JSON.stringify(e)} has no explicit ownership`);let f=p;if($(f,["config","service","state"],`profile ${e} ownership`),typeof f.config!="boolean"||typeof f.service!="boolean"||typeof f.state!="boolean")throw new n(`profile ${JSON.stringify(e)} has invalid ownership`);let l=typeof r.serviceName=="string"?r.serviceName.trim():"";if(e!=="default"&&!l||l&&!o.test(l))throw new n(`profile ${JSON.stringify(e)} has invalid serviceName`);return{label:c,host:"127.0.0.1",port:s,configPath:P(r.configPath,`profile ${e} configPath`),stateDir:P(r.stateDir,`profile ${e} stateDir`),serviceName:l,ownership:{config:f.config,service:f.service,state:f.state}}}function _(t,e){let o;try{o=A.readFileSync(t,"utf8")}catch(r){throw new n(`cannot read ${e} ${t}: ${r.message}`,"READ_FAILED")}try{let r=JSON.parse(o);if(!r||typeof r!="object"||Array.isArray(r))throw new Error("expected an object");return r}catch(r){throw new n(`${e} ${t} is corrupt JSON: ${r.message}`,"CORRUPT_JSON")}}function w(t){try{return A.realpathSync(t)}catch{return H(t)}}function Y(t,e=process.env,o=L()){if(!t||e.OURS_CONFIG||e.OURS_PORT||e.OURS_STATE_DIR)return null;let r=z(e,o);if(!A.existsSync(r))return null;let c=_(r,"installer profile registry");if($(c,["version","profiles","harnessAssociations"],"registry"),c.version!==E)throw new n(`registry version is ${JSON.stringify(c.version)}, expected ${E}`,"UNSUPPORTED_VERSION");let i=c.profiles,s=c.harnessAssociations;if(!i||typeof i!="object"||Array.isArray(i)||!s||typeof s!="object"||Array.isArray(s))throw new n("registry schema is invalid");let p=i,f=s,l=Object.fromEntries(Object.entries(p).map(([g,d])=>[g,T(d,g)])),h=Object.entries(l);for(let g=0;g<h.length;g+=1){let[d,N]=h[g];for(let[v,O]of h.slice(g+1)){let I=N.port===O.port?"endpoint":w(N.stateDir)===w(O.stateDir)?"stateDir":N.configPath===O.configPath?"configPath":N.serviceName===O.serviceName?"serviceName":"";if(I)throw new n(`profiles ${d} and ${v} collide on ${I}`,"PROFILE_COLLISION")}}for(let[g,d]of Object.entries(f))if(!x.includes(g)||typeof d!="string"||!(d in p))throw new n(`registry contains invalid association ${JSON.stringify(g)} -> ${JSON.stringify(d)}`);let a=f[t];if(a===void 0)return null;if(typeof a!="string"||!(a in p))throw new n(`${t} names missing profile ${JSON.stringify(a)}`);let u=T(p[a],a),m=_(u.configPath,`associated daemon config for ${a}`),y=b(m,o),S=w(y.stateDir);if(y.port!==u.port)throw new n(`profile ${a} expects port ${u.port} but ${u.configPath} resolves port ${y.port}`,"CONFIG_DRIFT");if(S!==w(u.stateDir))throw new n(`profile ${a} expects stateDir ${u.stateDir} but ${u.configPath} resolves ${S}`,"CONFIG_DRIFT");return{application:t,profileId:a,profile:u,registryPath:r}}async function tt(t,e,o,{fetch:r=globalThis.fetch,timeoutMs:c=2e3}={}){if(!t)return;let{profileId:i,profile:s}=t;if(e.port!==s.port||w(e.stateDir)!==w(s.stateDir))throw new n(`resolved runtime config drifted from profile ${i}`,"CONFIG_DRIFT");let p=async(a,u)=>{let m=new AbortController,y=setTimeout(()=>m.abort(),c);try{return await r(`http://127.0.0.1:${s.port}${a}`,{headers:u,signal:m.signal})}finally{clearTimeout(y)}},f;try{f=await p("/info")}catch(a){throw new n(`profile ${i} daemon is unreachable on 127.0.0.1:${s.port}: ${a.message}`,"UNREACHABLE")}if(!f.ok)throw new n(`profile ${i} /info returned HTTP ${f.status}`,"INCOMPATIBLE_DAEMON");let l=await f.json();if(l?.name!=="ours"||typeof l?.stateDir!="string")throw new n(`profile ${i} endpoint is not an ours daemon`,"INCOMPATIBLE_DAEMON");if(w(l.stateDir)!==w(s.stateDir))throw new n(`profile ${i} expects stateDir ${s.stateDir} but the daemon reports ${l.stateDir}`,"DAEMON_DRIFT");let h;try{h=await p("/identities",o?{"x-ours-api-token":o}:void 0)}catch(a){throw new n(`profile ${i} protected API probe failed: ${a.message}`,"AUTH_PROBE_FAILED")}if(h.status===401||h.status===403)throw new n(`profile ${i} rejected its selected authentication`,"AUTH_FAILED");if(!h.ok)throw new n(`profile ${i} protected API returned HTTP ${h.status}`,"INCOMPATIBLE_DAEMON")}export{x as ASSOCIATED_APPLICATIONS,n as AssociationError,E as INSTALLER_PROFILES_VERSION,X as assertApplicationCommand,z as installerProfilesPath,Q as parseApplicationArgs,Y as resolveRuntimeAssociation,tt as verifyRuntimeAssociation};