watchmyagents 0.8.0 → 0.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watchmyagents",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Security observability + real-time policy enforcement for AI agents. Local-first NDJSON capture with a continuous Watch daemon that auto-uploads anonymized signals, Shield CLI that blocks policy violations live (with policies pulled from Fortress cloud), anonymizer producing signals-only payloads, bidirectional sync with WatchMyAgents Fortress, and one-command install as an always-on launchd/systemd service — closing the recursive Watch→Guardian→Shield security loop.",
5
5
  "type": "module",
6
6
  "files": [
@@ -35,9 +35,7 @@
35
35
  "node": ">=18.0.0"
36
36
  },
37
37
  "dependencies": {},
38
- "devDependencies": {
39
- "@anthropic-ai/sdk": "^0.42.0"
40
- },
38
+ "devDependencies": {},
41
39
  "keywords": [
42
40
  "ai",
43
41
  "agents",
@@ -94,11 +94,15 @@ function launcherPath(label) { return join(CONFIG_DIR, `${label}.launcher.sh`);
94
94
 
95
95
  function writeLauncher(label, scriptPath, args) {
96
96
  const argLine = args.map(sh).join(' ');
97
+ // Load secrets with a read-loop, NOT '. file' / source. Sourcing would
98
+ // shell-evaluate each value, so a secret like FOO=https://x/$(cmd) would
99
+ // execute cmd at launch. A read-loop assigns the value literally — the
100
+ // content is never re-scanned for command substitution.
97
101
  const body = `#!/bin/sh
98
- # Generated by wma-service. Loads secrets then runs the WMA daemon.
99
- set -a
100
- . ${sh(ENV_FILE)}
101
- set +a
102
+ # Generated by wma-service. Loads secrets WITHOUT shell-evaluating their values.
103
+ while IFS='=' read -r __k __v; do
104
+ [ -n "$__k" ] && export "$__k=$__v"
105
+ done < ${sh(ENV_FILE)}
102
106
  exec ${sh(NODE)} ${sh(scriptPath)} ${argLine}
103
107
  `;
104
108
  const p = launcherPath(label);
@@ -147,15 +151,35 @@ function launchctl(args, { ignoreError = false } = {}) {
147
151
  }
148
152
  }
149
153
 
154
+ // Synchronous sleep (installer CLI — blocking is fine). Used to let launchd's
155
+ // asynchronous bootout finish before we bootstrap again.
156
+ function syncSleep(ms) {
157
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
158
+ }
159
+ function macLoaded(label) {
160
+ try { execFileSync('launchctl', ['print', `gui/${UID}/${label}`], { stdio: 'pipe' }); return true; }
161
+ catch { return false; }
162
+ }
163
+
150
164
  function macLoad(label, plist) {
151
165
  const domain = `gui/${UID}`;
152
- launchctl(['bootout', `${domain}/${label}`], { ignoreError: true }); // unload prior
153
- const ok = launchctl(['bootstrap', domain, plist]);
166
+ // bootout is async: on reinstall, bootstrapping again before the old instance
167
+ // is gone races and silently fails (symptom: reinstall = dead services).
168
+ // Wait for the prior instance to disappear, then retry bootstrap.
169
+ if (macLoaded(label)) {
170
+ launchctl(['bootout', `${domain}/${label}`], { ignoreError: true });
171
+ for (let i = 0; i < 20 && macLoaded(label); i++) syncSleep(150);
172
+ }
173
+ let ok = false;
174
+ for (let attempt = 0; attempt < 5 && !ok; attempt++) {
175
+ ok = launchctl(['bootstrap', domain, plist], { ignoreError: attempt < 4 });
176
+ if (!ok) syncSleep(250);
177
+ }
154
178
  launchctl(['enable', `${domain}/${label}`], { ignoreError: true });
155
179
  if (ok) info(`loaded ${label} (launchd) — running now + at every login`);
156
180
  else {
157
181
  warn(`could not auto-load ${label}. Load it manually:`);
158
- process.stdout.write(` launchctl bootstrap gui/${UID} ${plist}\n`);
182
+ process.stdout.write(` launchctl bootout gui/${UID}/${label} 2>/dev/null; launchctl bootstrap gui/${UID} ${plist}\n`);
159
183
  }
160
184
  }
161
185
 
package/scripts/shield.js CHANGED
@@ -36,7 +36,7 @@ import { DecisionLogger } from '../src/shield/decisions.js';
36
36
  import { listSessions } from '../src/sources/anthropic-managed.js';
37
37
  import { FortressPolicySource, postDecision } from '../src/shield/sources/fortress.js';
38
38
  import { resolveFortressBase } from '../src/fortress/url.js';
39
- import { isValidAgentId } from '../src/validate.js';
39
+ import { isValidAgentId, isValidSessionId } from '../src/validate.js';
40
40
 
41
41
  function parseArgs(argv) {
42
42
  const out = {};
@@ -429,6 +429,11 @@ async function main() {
429
429
  if (!isValidAgentId(agentId)) {
430
430
  die(`error: --agent-id has invalid format (expected "agent_" + alphanumeric, got "${agentId}")`);
431
431
  }
432
+ // --session-id ends up in the Anthropic SSE URL path (src/shield/stream.js).
433
+ // Validate the same way wma-fetch does so a crafted value can't tamper the URL.
434
+ if (singleSessionId && !isValidSessionId(singleSessionId)) {
435
+ die(`error: --session-id has invalid format (expected "sesn_" + alphanumeric, got "${singleSessionId}")`);
436
+ }
432
437
 
433
438
  // Policies source: --policies-source fortress | local (default infers from --policy)
434
439
  let ruleset; // for 'local' mode: static; for 'fortress': initial snapshot