@volter/teams 0.2.3 → 0.2.5

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.
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ // The board's native PreToolUse command policy. No process lookup or signal occurs here.
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { resolve } from 'node:path';
6
+ export const processGuardReason = 'Pattern kills are refused for board sessions. Kill only the PID this session recorded when it started that process; use the owning service door for a service.';
7
+ export function patternKill(command) {
8
+ // Inspect shell bodies as well as nested `sh -c` / substitutions. Removing shell
9
+ // escapes and quotes catches split words, paths and quoted embedded commands.
10
+ const text = String(command).replace(/\\\r?\n/g, '').replace(/\\([a-zA-Z-])/g, '$1').replace(/["']/g, '');
11
+ const head = String.raw`(?:^|[;|&(\n{}]|\s-[a-zA-Z]*c\s+)\s*(?:(?:env|sudo|exec|command|xargs|nohup|do|then|else|!)\s+(?:--?[^\s;|&()]+\s+)*|[A-Za-z_][A-Za-z0-9_]*=[^\s;|&()]+\s+)*(?:[\w./-]+/)?`;
12
+ return new RegExp(head + String.raw`(?:pkill|killall)(?=$|[\s;|&()])`).test(text) ||
13
+ new RegExp(head + String.raw`pgrep\s+[^;|&\n)]*(?:--full\b|-[a-zA-Z]*f[a-zA-Z]*(?=$|\s))`).test(text);
14
+ }
15
+ const quote = value => `'${String(value).replaceAll("'", "'\\''")}'`;
16
+ export function boardGuardArguments(kind, args, { cwd, enabled, node = process.execPath } = {}) {
17
+ if (!enabled) return args;
18
+ const command = `${quote(node)} ${quote(fileURLToPath(import.meta.url))}`;
19
+ const hooks = [{ matcher: '^Bash$', hooks: [{ type: 'command', command, timeout: 10 }] }];
20
+ if (kind === 'claude-code') {
21
+ const next = [...args], at = next.findIndex(arg => arg === '--settings' || arg.startsWith('--settings='));
22
+ if (at < 0) return [...next, '--settings', JSON.stringify({ hooks: { PreToolUse: hooks } })];
23
+ const inline = next[at].startsWith('--settings='), value = inline ? next[at].slice(11) : next[at + 1];
24
+ const settings = JSON.parse(value?.trimStart().startsWith('{') ? value : readFileSync(resolve(cwd || process.cwd(), value), 'utf8'));
25
+ settings.hooks ??= {}; settings.hooks.PreToolUse = [...(settings.hooks.PreToolUse ?? []), ...hooks];
26
+ next.splice(at, inline ? 1 : 2, '--settings', JSON.stringify(settings));
27
+ return next;
28
+ }
29
+ if (kind === 'codex') {
30
+ // Native Codex rules cannot split substitutions such as kill $(pgrep -f …).
31
+ // Its synchronous Bash hook can deny the entire command before execution.
32
+ // This invocation enables and trusts the board-vetted command policy.
33
+ const handler = `{ matcher = "^Bash$", hooks = [{ type = "command", command = ${JSON.stringify(command)}, timeout = 10 }] }`;
34
+ // Codex's own flags go before any subcommand (`app-server`, `resume`) or prompt: after `app-server`, Codex
35
+ // 0.159 refuses --dangerously-bypass-hook-trust and the worker never starts.
36
+ return [...(args.includes('--dangerously-bypass-hook-trust') ? [] : ['--dangerously-bypass-hook-trust']), '-c', 'features.hooks=true', '-c', `hooks.PreToolUse=[${handler}]`, ...args];
37
+ }
38
+ throw new Error(`board process guard is unavailable for ${kind}; no unguarded session was launched`);
39
+ }
40
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
41
+ try {
42
+ const input = JSON.parse(readFileSync(0, 'utf8'));
43
+ const command = input.tool_input?.command ?? input.tool_input?.cmd;
44
+ if (typeof command !== 'string') throw new Error('Bash hook input has no command');
45
+ if (patternKill(command)) process.stdout.write(JSON.stringify({ hookSpecificOutput: {
46
+ hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: processGuardReason,
47
+ } }) + '\n');
48
+ } catch (error) {
49
+ process.stderr.write(`Board process policy could not read the command: ${error.message}. ${processGuardReason}\n`);
50
+ process.exitCode = 2;
51
+ }
52
+ }
@@ -0,0 +1,3 @@
1
+ // What one published event may carry: the server refuses a source event whose `data` serializes above this many
2
+ // bytes, and a publisher fits its snapshot to it before sending.
3
+ export const EVENT_DATA_LIMIT = 262144;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
6
6
  "license": "MIT",
@@ -45,8 +45,10 @@
45
45
  },
46
46
  "./context": "./context.mjs",
47
47
  "./event-client": "./event-client.mjs",
48
+ "./event-limits": "./event-limits.mjs",
48
49
  "./machine-door": "./machine-door.mjs",
49
50
  "./sender-identity": "./sender-identity.mjs",
51
+ "./board-process-guard": "./board-process-guard.mjs",
50
52
  "./server/agent-authority": "./server/agent-authority.mjs"
51
53
  },
52
54
  "files": [
@@ -1,4 +1,6 @@
1
1
  // Live agency and delegation are evaluated from the store; identity labels and environment hints grant nothing.
2
+ // A seed's standing people are a CTE, not a subquery nested under the chain's: a hosted Teams server (a Durable
3
+ // Object) prepares SQL under SQLite's expression-depth cap of 100, and this query is nested inside others (auth.mjs).
2
4
  export const AGENT_ACTIVE = `WITH RECURSIVE scope(team,id) AS (VALUES(?,?)), chain(id) AS (
3
5
  SELECT a.id FROM agents a,scope s WHERE a.team_id=s.team AND a.id=s.id
4
6
  UNION SELECT a.parent_id FROM agents a JOIN chain c ON a.id=c.id,scope s WHERE a.team_id=s.team AND a.parent_id IS NOT NULL
@@ -12,10 +14,11 @@ export const AGENT_ACTIVE = `WITH RECURSIVE scope(team,id) AS (VALUES(?,?)), cha
12
14
  UNION SELECT g.seed,json_extract(CASE WHEN j.type='object' THEN j.value ELSE '{}' END,'$.id') FROM agents a JOIN ancestry g ON a.id=g.id,scope s,json_tree(a.lineage_json,'$.starter') j WHERE a.team_id=s.team AND j.type='object' AND json_extract(CASE WHEN j.type='object' THEN j.value ELSE '{}' END,'$.kind')='person'
13
15
  UNION SELECT g.seed,ps.sponsor_id FROM agents a JOIN ancestry g ON a.id=g.id JOIN json_tree(a.lineage_json,'$.starter') j ON j.type='object' JOIN principal_sponsors ps ON ps.team_id=a.team_id AND ps.principal_id=json_extract(CASE WHEN j.type='object' THEN j.value ELSE '{}' END,'$.id'),scope s WHERE a.team_id=s.team
14
16
  UNION SELECT g.seed,i.installer_id FROM agents a JOIN ancestry g ON a.id=g.id JOIN json_tree(a.lineage_json,'$.starter') j ON j.type='object' JOIN app_installations i ON i.team_id=a.team_id AND i.principal_id=json_extract(CASE WHEN j.type='object' THEN j.value ELSE '{}' END,'$.id') AND i.state='active' JOIN apps app ON app.id=i.app_id AND app.state='active',scope s WHERE a.team_id=s.team
17
+ ), standing(seed) AS (
18
+ SELECT x.seed FROM people x JOIN principals p ON p.id=x.id,scope s WHERE x.seed IS NOT NULL AND p.kind='user' AND
19
+ (EXISTS(SELECT 1 FROM memberships m WHERE m.team_id=s.team AND m.principal_id=p.id AND m.active=1)
20
+ OR EXISTS(SELECT 1 FROM identity_principals ip WHERE ip.team_id=s.team AND ip.principal_id=p.id)
21
+ AND NOT EXISTS(SELECT 1 FROM memberships m WHERE m.team_id=s.team AND m.principal_id=p.id AND m.active=0))
15
22
  ) SELECT 1 FROM agents root,scope s WHERE root.id=s.id AND root.team_id=s.team
16
- AND NOT EXISTS(SELECT 1 FROM chain c LEFT JOIN agents a ON a.id=c.id AND a.team_id=s.team WHERE a.id IS NULL OR a.state!='active' OR NOT EXISTS(
17
- SELECT 1 FROM people x JOIN principals p ON p.id=x.id WHERE x.seed=a.id AND p.kind='user' AND
18
- (EXISTS(SELECT 1 FROM memberships m WHERE m.team_id=s.team AND m.principal_id=p.id AND m.active=1)
19
- OR EXISTS(SELECT 1 FROM identity_principals ip WHERE ip.team_id=s.team AND ip.principal_id=p.id)
20
- AND NOT EXISTS(SELECT 1 FROM memberships m WHERE m.team_id=s.team AND m.principal_id=p.id AND m.active=0))))`;
23
+ AND NOT EXISTS(SELECT 1 FROM chain c LEFT JOIN agents a ON a.id=c.id AND a.team_id=s.team WHERE a.id IS NULL OR a.state!='active' OR a.id NOT IN (SELECT seed FROM standing))`;
21
24
  export const AGENT_DESCENDANTS = `WITH RECURSIVE tree(id) AS (SELECT id FROM agents WHERE team_id=? AND id=? UNION SELECT a.id FROM agents a JOIN tree t ON a.parent_id=t.id) SELECT id FROM tree`;
package/server/auth.mjs CHANGED
@@ -14,8 +14,10 @@ export const APP_LINEAGE = `WITH RECURSIVE lineage(id) AS (
14
14
  UNION SELECT d.parent_credential_id FROM agent_credentials d JOIN lineage l ON d.credential_id=l.id
15
15
  UNION SELECT ac.subject_credential_id FROM app_credentials ac JOIN lineage l ON ac.credential_id=l.id WHERE ac.subject_credential_id IS NOT NULL
16
16
  )`;
17
- export const APP_LINEAGE_REVOKED = `${APP_LINEAGE}
18
- SELECT 1 FROM lineage l JOIN credentials c ON c.id=l.id
17
+ // One compound term per way a lineage is revoked, and each agent's standing over a flat list of the lineage's agents
18
+ // and actors: a hosted Teams server (a Durable Object) prepares SQL under SQLite's expression-depth cap of 100, and the
19
+ // one OR over nested AGENT_ACTIVE subqueries needed about 125.
20
+ const LINEAGE_ROWS = `FROM lineage l JOIN credentials c ON c.id=l.id
19
21
  JOIN principals p ON p.id=c.principal_id
20
22
  LEFT JOIN memberships m ON m.team_id=c.team_id AND m.principal_id=c.principal_id
21
23
  LEFT JOIN app_credentials ac ON ac.credential_id=c.id
@@ -23,16 +25,21 @@ export const APP_LINEAGE_REVOKED = `${APP_LINEAGE}
23
25
  LEFT JOIN app_installations i ON i.id=COALESCE(ac.installation_id,ic.installation_id)
24
26
  LEFT JOIN apps a ON a.id=i.app_id
25
27
  LEFT JOIN memberships installer ON installer.team_id=i.team_id AND installer.principal_id=i.installer_id
26
- LEFT JOIN memberships service ON service.team_id=i.team_id AND service.principal_id=i.principal_id
27
- WHERE (p.kind='agent' AND NOT EXISTS (${AGENT_ACTIVE.replace('VALUES(?,?)','SELECT c.team_id,c.principal_id')}))
28
- OR EXISTS(SELECT 1 FROM json_each(COALESCE(ac.actor_json,'[]')) actor
29
- JOIN agents named ON named.team_id=c.team_id AND named.id=actor.value
30
- WHERE actor.type='text' AND NOT EXISTS (${AGENT_ACTIVE.replace('VALUES(?,?)','SELECT named.team_id,named.id')}))
31
- OR EXISTS(SELECT 1 FROM agent_credentials d LEFT JOIN memberships dm ON dm.team_id=d.team_id AND dm.principal_id=c.principal_id WHERE d.parent_credential_id=c.id AND d.credential_id IN(SELECT id FROM lineage) AND COALESCE(dm.active,0)!=1)
28
+ LEFT JOIN memberships service ON service.team_id=i.team_id AND service.principal_id=i.principal_id`;
29
+ export const APP_LINEAGE_REVOKED = `${APP_LINEAGE}, lineage_actors(team,id) AS (
30
+ SELECT c.team_id,actor.value FROM lineage l JOIN credentials c ON c.id=l.id JOIN principals p ON p.id=c.principal_id
31
+ JOIN app_credentials ac ON ac.credential_id=c.id, json_each(COALESCE(ac.actor_json,'[]')) actor WHERE actor.type='text')
32
+ SELECT 1 FROM lineage l JOIN credentials c ON c.id=l.id JOIN principals p ON p.id=c.principal_id
33
+ WHERE p.kind='agent' AND NOT EXISTS (${AGENT_ACTIVE.replace('VALUES(?,?)','SELECT c.team_id,c.principal_id')})
34
+ UNION ALL SELECT 1 FROM lineage_actors x JOIN agents named ON named.team_id=x.team AND named.id=x.id
35
+ WHERE NOT EXISTS (${AGENT_ACTIVE.replace('VALUES(?,?)','SELECT named.team_id,named.id')})
36
+ UNION ALL SELECT 1 ${LINEAGE_ROWS}
37
+ WHERE EXISTS(SELECT 1 FROM agent_credentials d LEFT JOIN memberships dm ON dm.team_id=d.team_id AND dm.principal_id=c.principal_id WHERE d.parent_credential_id=c.id AND d.credential_id IN(SELECT id FROM lineage) AND COALESCE(dm.active,0)!=1)
32
38
  OR c.revoked_at IS NOT NULL OR (c.expires_at IS NOT NULL AND c.expires_at<=?)
33
39
  OR (c.team_id IS NOT NULL AND COALESCE(m.active,0)!=1)
34
40
  OR (i.id IS NOT NULL AND (i.state!='active' OR a.state!='active' OR COALESCE(installer.active,0)!=1
35
- OR installer.role NOT IN ('owner','admin') OR COALESCE(service.active,0)!=1)) LIMIT 1`;
41
+ OR installer.role NOT IN ('owner','admin') OR COALESCE(service.active,0)!=1))
42
+ LIMIT 1`;
36
43
  export function canRead(auth, session, grants = []) {
37
44
  if (!auth || !['user','service'].includes(auth.kind) || !auth.active || session.deleted_at != null) return false;
38
45
  if (!appGrantAllows(auth, 'session', session.id, 'sessions.read')) return false;
@@ -5,6 +5,7 @@ import { exact } from './request-json.mjs';
5
5
  import { canonicalJson } from './wire.mjs';
6
6
  import { writeSse } from './events.mjs';
7
7
  import { APP_EVENTS, PUBLISHABLE_EVENTS, eventReadAction } from './event-catalogue.mjs';
8
+ import { EVENT_DATA_LIMIT } from '../event-limits.mjs';
8
9
 
9
10
  const stmt = (sql, ...params) => ({sql, params});
10
11
  const fail = (status,code,message) => {throw Object.assign(new Error(message),{status,code});};
@@ -69,7 +70,7 @@ export async function publishedEventRoute(ctx) {
69
70
  if(publication&&req.method==='POST'){
70
71
  const body=await parseJson(req);exact(body,['id','kind','resource','revision','data','actor']);
71
72
  text(body.id,'id');text(body.resource?.id,'resource.id');exact(body.resource,['kind','id']);
72
- if(!PUBLISHABLE_EVENTS.includes(body.kind)||body.resource.kind!==body.kind.split('.')[0]||!body.resource.id.startsWith(`${publication[1]}:`)||!Number.isSafeInteger(body.revision)||body.revision<1||!body.data||typeof body.data!=='object'||Array.isArray(body.data)||Buffer.byteLength(JSON.stringify(body.data))>262144)fail(400,'invalid_request','invalid source event or snapshot');
73
+ if(!PUBLISHABLE_EVENTS.includes(body.kind)||body.resource.kind!==body.kind.split('.')[0]||!body.resource.id.startsWith(`${publication[1]}:`)||!Number.isSafeInteger(body.revision)||body.revision<1||!body.data||typeof body.data!=='object'||Array.isArray(body.data)||Buffer.byteLength(JSON.stringify(body.data))>EVENT_DATA_LIMIT)fail(400,'invalid_request','invalid source event or snapshot');
73
74
  // A source can prove only its own principal. A plain session is evidence, never principal identity.
74
75
  const actor={principal:auth.principal_id,session:null,lineage:auth.act??[]};
75
76
  if(body.actor!=null){exact(body.actor,['session']);if(body.actor.session!=null){text(body.actor.session,'actor.session');if(!body.actor.session.startsWith('session:'))fail(400,'invalid_request','actor.session must name a session');actor.session=body.actor.session;}}