@nocville/npc 0.3.1 → 0.4.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/README.md CHANGED
@@ -226,3 +226,22 @@ class MyNPC extends PollingNPC {
226
226
 
227
227
  `nocville-npc --help` lists every command and flag, and `nocville-npc doctor` checks that your
228
228
  key and connection work. More at [nocville.com](https://nocville.com).
229
+
230
+ ## Encrypted Cloudflare workspaces
231
+
232
+ Download and verify the producer workspace settings in team privacy settings, then:
233
+
234
+ ```sh
235
+ nocville-npc init --workspace=/absolute/path/workspace.json
236
+ # Supply the agent key through NOCVILLE_API_KEY or the existing init prompt.
237
+ nocville-npc system-monitor
238
+ ```
239
+
240
+ `--workspace`, `NOCVILLE_WORKSPACE`, and the saved workspace path are supported in
241
+ that order. Constructor `workspace` overrides them. The first publication displays
242
+ a signing-device fingerprint for the team owner to approve. Approval is shared by
243
+ the local NPCs using this credential, including coding-agent hooks and watchdogs.
244
+ The workspace pins the HTTPS origin and opaque map ID; changing either requires
245
+ matching verified workspace settings. The `doctor` command uses encrypted enrollment
246
+ and an offline probe for these workspaces. The Node SDK README describes private
247
+ profile storage and the encryption boundary.
package/dist/cjs/cli.js CHANGED
@@ -96,6 +96,7 @@ Monitoring NPC types:
96
96
  web-page-watcher Render a watched web page as a pixel-art TV
97
97
 
98
98
  Common Options:
99
+ --workspace=<file> Verified producer workspace settings from team privacy settings.
99
100
  --api-key=<key> Team API key (or NOCVILLE_API_KEY env)
100
101
  --api-url=<url> Nocville API URL (or NOCVILLE_API_URL env).
101
102
  Default: https://nocville.com. For local dev use
@@ -512,7 +513,20 @@ async function runInit(flags, config) {
512
513
  let apiKey = (0, config_js_1.stringFlag)(flags, 'api-key');
513
514
  const apiUrl = (0, config_js_1.stringFlag)(flags, 'api-url');
514
515
  const map = (0, config_js_1.stringFlag)(flags, 'map');
515
- if (!apiKey && !apiUrl && !map) {
516
+ const workspace = config.workspace;
517
+ if (workspace)
518
+ apiKey = apiKey || config.apiKey;
519
+ if (workspace && !apiKey) {
520
+ const { resolveApiKey } = await Promise.resolve().then(() => __importStar(require('./core/api-key.js')));
521
+ const resolved = await resolveApiKey({ flags });
522
+ if (!resolved.ok) {
523
+ console.error(resolved.error);
524
+ process.exitCode = 1;
525
+ return;
526
+ }
527
+ apiKey = resolved.key;
528
+ }
529
+ if (!apiKey && !apiUrl && !map && !workspace) {
516
530
  const saved = loadUserConfig();
517
531
  if (saved.apiKey && !process.env['NOCVILLE_API_KEY']?.trim()) {
518
532
  console.log('Usage: nocville-npc init --api-key=<key> [--api-url=<url>] [--map=<name>]');
@@ -528,7 +542,12 @@ async function runInit(flags, config) {
528
542
  }
529
543
  apiKey = resolved.key;
530
544
  }
531
- const path = saveUserConfig({ apiKey, apiUrl, map });
545
+ const path = saveUserConfig({
546
+ apiKey,
547
+ apiUrl: workspace ? config.apiUrl : apiUrl,
548
+ map: workspace ? config.map : map,
549
+ workspace,
550
+ });
532
551
  const saved = loadUserConfig();
533
552
  console.log(`✓ Saved ${path} (mode 0600)`);
534
553
  console.log(` API: ${saved.apiUrl ?? 'https://nocville.com'} key: ${maskKey(saved.apiKey)}`);
@@ -17,6 +17,7 @@ class BaseNPC {
17
17
  // We can't access `this.name` here since it's abstract, so use a placeholder
18
18
  this.logger = new logger_js_1.Logger('npc', config.debug);
19
19
  this.client = new node_1.NocvilleClient({
20
+ workspace: config.workspace,
20
21
  apiUrl: config.apiUrl,
21
22
  apiKey: config.apiKey,
22
23
  serviceId: config.serviceId,
@@ -6,6 +6,8 @@ exports.stringFlag = stringFlag;
6
6
  exports.boolFlag = boolFlag;
7
7
  exports.listFlag = listFlag;
8
8
  exports.numberFlag = numberFlag;
9
+ const node_1 = require("@nocville/node");
10
+ const node_path_1 = require("node:path");
9
11
  const user_config_js_1 = require("./user-config.js");
10
12
  /**
11
13
  * Simple hand-rolled CLI arg parser.
@@ -56,9 +58,16 @@ function parseArgs(argv) {
56
58
  */
57
59
  function resolveConfig(flags, overrides) {
58
60
  const saved = (0, user_config_js_1.loadUserConfig)();
61
+ const workspacePath = overrides?.workspace ||
62
+ stringFlag(flags, 'workspace') ||
63
+ process.env['NOCVILLE_WORKSPACE'] ||
64
+ saved.workspace;
65
+ const workspace = workspacePath ? (0, node_path_1.resolve)(workspacePath) : undefined;
66
+ const enrollment = workspace ? (0, node_1.loadProducerWorkspace)(workspace) : undefined;
59
67
  const apiUrl = overrides?.apiUrl ||
60
68
  stringFlag(flags, 'api-url') ||
61
69
  process.env['NOCVILLE_API_URL'] ||
70
+ enrollment?.apiUrl ||
62
71
  saved.apiUrl ||
63
72
  'https://nocville.com';
64
73
  const apiKey = overrides?.apiKey ||
@@ -76,10 +85,23 @@ function resolveConfig(flags, overrides) {
76
85
  const map = overrides?.map ||
77
86
  stringFlag(flags, 'map') ||
78
87
  process.env['NOCVILLE_MAP'] ||
88
+ enrollment?.encryption.mapId ||
79
89
  saved.map ||
80
90
  undefined;
81
91
  const name = overrides?.name || stringFlag(flags, 'name') || process.env['NOCVILLE_NAME'] || undefined;
82
- return { apiUrl, apiKey, serviceId, debug, map, name };
92
+ if (enrollment && apiUrl.replace(/\/+$/, '') !== enrollment.apiUrl)
93
+ throw new Error('API origin does not match the verified workspace');
94
+ if (enrollment && map && map !== enrollment.encryption.mapId)
95
+ throw new Error('Encrypted producers use the map ID in their workspace settings');
96
+ return {
97
+ apiUrl,
98
+ apiKey,
99
+ serviceId,
100
+ debug,
101
+ map: enrollment?.encryption.mapId ?? map,
102
+ name,
103
+ ...(workspace ? { workspace } : {}),
104
+ };
83
105
  }
84
106
  function stringFlag(flags, key) {
85
107
  const val = flags[key];
@@ -25,6 +25,8 @@ function loadUserConfig(path = userConfigPath()) {
25
25
  try {
26
26
  const raw = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf-8'));
27
27
  const out = {};
28
+ if (typeof raw['workspace'] === 'string' && raw['workspace'])
29
+ out.workspace = raw['workspace'];
28
30
  if (typeof raw['apiUrl'] === 'string' && raw['apiUrl'])
29
31
  out.apiUrl = raw['apiUrl'];
30
32
  if (typeof raw['apiKey'] === 'string' && raw['apiKey'])
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runDoctor = runDoctor;
4
4
  exports.hasCodexHookWatcherOverlap = hasCodexHookWatcherOverlap;
5
+ const node_1 = require("@nocville/node");
5
6
  const node_crypto_1 = require("node:crypto");
6
7
  const node_fs_1 = require("node:fs");
7
8
  const liveness_js_1 = require("./npcs/agent-common/liveness.js");
@@ -43,6 +44,24 @@ async function runDoctor(config) {
43
44
  if (!hasKey) {
44
45
  console.log(' [SKIP] POST /api/v1/status — no --api-key / NOCVILLE_API_KEY set');
45
46
  }
47
+ else if (config.workspace || config.apiKey.includes('.team_')) {
48
+ // Exercise the locally encrypted path; never send a plaintext probe to a v2 workspace.
49
+ try {
50
+ const client = new node_1.NocvilleClient({
51
+ workspace: config.workspace,
52
+ apiUrl: config.apiUrl,
53
+ apiKey: config.apiKey,
54
+ serviceId: 'nocville-doctor',
55
+ retries: 0,
56
+ });
57
+ const result = await client.pushStatus({ status: 'unknown', offline: true });
58
+ pushOk = result.success;
59
+ console.log(` [${pushOk ? 'OK' : 'FAIL'}] Encrypted producer: ${result.error ?? 'accepted'}`);
60
+ }
61
+ catch (error) {
62
+ console.log(` [FAIL] Encrypted producer: ${error.message}`);
63
+ }
64
+ }
46
65
  else {
47
66
  const start = Date.now();
48
67
  const t = withTimeout(5000);
@@ -263,6 +263,7 @@ function buildAgentPayload(adapter, state, config) {
263
263
  }
264
264
  async function defaultPush(config, serviceId, payload, timeoutMs) {
265
265
  const client = new node_1.NocvilleClient({
266
+ workspace: config.workspace,
266
267
  apiUrl: config.apiUrl,
267
268
  apiKey: config.apiKey,
268
269
  serviceId,
@@ -381,6 +382,8 @@ function spawnDetachedAgent(adapter, config, args, cliPath = process.argv[1]) {
381
382
  if (!cliPath)
382
383
  return undefined;
383
384
  const env = { ...process.env };
385
+ if (config.workspace)
386
+ env['NOCVILLE_WORKSPACE'] = config.workspace;
384
387
  env['NOCVILLE_API_URL'] = config.apiUrl;
385
388
  env['NOCVILLE_API_KEY'] = config.apiKey;
386
389
  if (config.map)
@@ -339,6 +339,7 @@ class CodexWatcher {
339
339
  let client = this.clients.get(serviceId);
340
340
  if (!client) {
341
341
  client = new node_1.NocvilleClient({
342
+ workspace: this.config.workspace,
342
343
  apiUrl: this.config.apiUrl,
343
344
  apiKey: this.config.apiKey,
344
345
  serviceId,
package/dist/cli.js CHANGED
@@ -61,6 +61,7 @@ Monitoring NPC types:
61
61
  web-page-watcher Render a watched web page as a pixel-art TV
62
62
 
63
63
  Common Options:
64
+ --workspace=<file> Verified producer workspace settings from team privacy settings.
64
65
  --api-key=<key> Team API key (or NOCVILLE_API_KEY env)
65
66
  --api-url=<url> Nocville API URL (or NOCVILLE_API_URL env).
66
67
  Default: https://nocville.com. For local dev use
@@ -477,7 +478,20 @@ async function runInit(flags, config) {
477
478
  let apiKey = stringFlag(flags, 'api-key');
478
479
  const apiUrl = stringFlag(flags, 'api-url');
479
480
  const map = stringFlag(flags, 'map');
480
- if (!apiKey && !apiUrl && !map) {
481
+ const workspace = config.workspace;
482
+ if (workspace)
483
+ apiKey = apiKey || config.apiKey;
484
+ if (workspace && !apiKey) {
485
+ const { resolveApiKey } = await import('./core/api-key.js');
486
+ const resolved = await resolveApiKey({ flags });
487
+ if (!resolved.ok) {
488
+ console.error(resolved.error);
489
+ process.exitCode = 1;
490
+ return;
491
+ }
492
+ apiKey = resolved.key;
493
+ }
494
+ if (!apiKey && !apiUrl && !map && !workspace) {
481
495
  const saved = loadUserConfig();
482
496
  if (saved.apiKey && !process.env['NOCVILLE_API_KEY']?.trim()) {
483
497
  console.log('Usage: nocville-npc init --api-key=<key> [--api-url=<url>] [--map=<name>]');
@@ -493,7 +507,12 @@ async function runInit(flags, config) {
493
507
  }
494
508
  apiKey = resolved.key;
495
509
  }
496
- const path = saveUserConfig({ apiKey, apiUrl, map });
510
+ const path = saveUserConfig({
511
+ apiKey,
512
+ apiUrl: workspace ? config.apiUrl : apiUrl,
513
+ map: workspace ? config.map : map,
514
+ workspace,
515
+ });
497
516
  const saved = loadUserConfig();
498
517
  console.log(`✓ Saved ${path} (mode 0600)`);
499
518
  console.log(` API: ${saved.apiUrl ?? 'https://nocville.com'} key: ${maskKey(saved.apiKey)}`);
@@ -14,6 +14,7 @@ export class BaseNPC {
14
14
  // We can't access `this.name` here since it's abstract, so use a placeholder
15
15
  this.logger = new Logger('npc', config.debug);
16
16
  this.client = new NocvilleClient({
17
+ workspace: config.workspace,
17
18
  apiUrl: config.apiUrl,
18
19
  apiKey: config.apiKey,
19
20
  serviceId: config.serviceId,
@@ -1,3 +1,5 @@
1
+ import { loadProducerWorkspace } from '@nocville/node';
2
+ import { resolve } from 'node:path';
1
3
  import { loadUserConfig } from './user-config.js';
2
4
  /**
3
5
  * Simple hand-rolled CLI arg parser.
@@ -48,9 +50,16 @@ export function parseArgs(argv) {
48
50
  */
49
51
  export function resolveConfig(flags, overrides) {
50
52
  const saved = loadUserConfig();
53
+ const workspacePath = overrides?.workspace ||
54
+ stringFlag(flags, 'workspace') ||
55
+ process.env['NOCVILLE_WORKSPACE'] ||
56
+ saved.workspace;
57
+ const workspace = workspacePath ? resolve(workspacePath) : undefined;
58
+ const enrollment = workspace ? loadProducerWorkspace(workspace) : undefined;
51
59
  const apiUrl = overrides?.apiUrl ||
52
60
  stringFlag(flags, 'api-url') ||
53
61
  process.env['NOCVILLE_API_URL'] ||
62
+ enrollment?.apiUrl ||
54
63
  saved.apiUrl ||
55
64
  'https://nocville.com';
56
65
  const apiKey = overrides?.apiKey ||
@@ -68,10 +77,23 @@ export function resolveConfig(flags, overrides) {
68
77
  const map = overrides?.map ||
69
78
  stringFlag(flags, 'map') ||
70
79
  process.env['NOCVILLE_MAP'] ||
80
+ enrollment?.encryption.mapId ||
71
81
  saved.map ||
72
82
  undefined;
73
83
  const name = overrides?.name || stringFlag(flags, 'name') || process.env['NOCVILLE_NAME'] || undefined;
74
- return { apiUrl, apiKey, serviceId, debug, map, name };
84
+ if (enrollment && apiUrl.replace(/\/+$/, '') !== enrollment.apiUrl)
85
+ throw new Error('API origin does not match the verified workspace');
86
+ if (enrollment && map && map !== enrollment.encryption.mapId)
87
+ throw new Error('Encrypted producers use the map ID in their workspace settings');
88
+ return {
89
+ apiUrl,
90
+ apiKey,
91
+ serviceId,
92
+ debug,
93
+ map: enrollment?.encryption.mapId ?? map,
94
+ name,
95
+ ...(workspace ? { workspace } : {}),
96
+ };
75
97
  }
76
98
  export function stringFlag(flags, key) {
77
99
  const val = flags[key];
@@ -6,6 +6,8 @@ export type Archetype = 'service' | 'database' | 'trading' | 'ai-agent' | 'pipel
6
6
  /** A companion declaration is the SDK's CompanionSpec. */
7
7
  export type CompanionDecl = CompanionSpec;
8
8
  export interface NpcConfig {
9
+ /** Local verified workspace enrollment bundle. */
10
+ workspace?: string;
9
11
  apiUrl: string;
10
12
  apiKey: string;
11
13
  serviceId: string;
@@ -4,6 +4,7 @@
4
4
  * has to appear in ~/.claude/settings.json, ~/.codex/hooks.json or `ps` output.
5
5
  */
6
6
  export interface UserConfig {
7
+ workspace?: string;
7
8
  apiUrl?: string;
8
9
  apiKey?: string;
9
10
  map?: string;
@@ -18,6 +18,8 @@ export function loadUserConfig(path = userConfigPath()) {
18
18
  try {
19
19
  const raw = JSON.parse(readFileSync(path, 'utf-8'));
20
20
  const out = {};
21
+ if (typeof raw['workspace'] === 'string' && raw['workspace'])
22
+ out.workspace = raw['workspace'];
21
23
  if (typeof raw['apiUrl'] === 'string' && raw['apiUrl'])
22
24
  out.apiUrl = raw['apiUrl'];
23
25
  if (typeof raw['apiKey'] === 'string' && raw['apiKey'])
package/dist/doctor.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { NocvilleClient } from '@nocville/node';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { readdirSync } from 'node:fs';
3
4
  import { isAgentAlive } from './npcs/agent-common/liveness.js';
@@ -39,6 +40,24 @@ export async function runDoctor(config) {
39
40
  if (!hasKey) {
40
41
  console.log(' [SKIP] POST /api/v1/status — no --api-key / NOCVILLE_API_KEY set');
41
42
  }
43
+ else if (config.workspace || config.apiKey.includes('.team_')) {
44
+ // Exercise the locally encrypted path; never send a plaintext probe to a v2 workspace.
45
+ try {
46
+ const client = new NocvilleClient({
47
+ workspace: config.workspace,
48
+ apiUrl: config.apiUrl,
49
+ apiKey: config.apiKey,
50
+ serviceId: 'nocville-doctor',
51
+ retries: 0,
52
+ });
53
+ const result = await client.pushStatus({ status: 'unknown', offline: true });
54
+ pushOk = result.success;
55
+ console.log(` [${pushOk ? 'OK' : 'FAIL'}] Encrypted producer: ${result.error ?? 'accepted'}`);
56
+ }
57
+ catch (error) {
58
+ console.log(` [FAIL] Encrypted producer: ${error.message}`);
59
+ }
60
+ }
42
61
  else {
43
62
  const start = Date.now();
44
63
  const t = withTimeout(5000);
@@ -241,6 +241,7 @@ export function buildAgentPayload(adapter, state, config) {
241
241
  }
242
242
  async function defaultPush(config, serviceId, payload, timeoutMs) {
243
243
  const client = new NocvilleClient({
244
+ workspace: config.workspace,
244
245
  apiUrl: config.apiUrl,
245
246
  apiKey: config.apiKey,
246
247
  serviceId,
@@ -359,6 +360,8 @@ export function spawnDetachedAgent(adapter, config, args, cliPath = process.argv
359
360
  if (!cliPath)
360
361
  return undefined;
361
362
  const env = { ...process.env };
363
+ if (config.workspace)
364
+ env['NOCVILLE_WORKSPACE'] = config.workspace;
362
365
  env['NOCVILLE_API_URL'] = config.apiUrl;
363
366
  env['NOCVILLE_API_KEY'] = config.apiKey;
364
367
  if (config.map)
@@ -335,6 +335,7 @@ export class CodexWatcher {
335
335
  let client = this.clients.get(serviceId);
336
336
  if (!client) {
337
337
  client = new NocvilleClient({
338
+ workspace: this.config.workspace,
338
339
  apiUrl: this.config.apiUrl,
339
340
  apiKey: this.config.apiKey,
340
341
  serviceId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocville/npc",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Put your Claude Code and Codex sessions (and your servers) in the Nocville office as live NPCs",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "dependencies": {
26
26
  "pngjs": "^7.0.0",
27
- "@nocville/node": "^0.3.1"
27
+ "@nocville/node": "^0.4.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "puppeteer-core": "*"