@venturewild/workspace 0.3.6 → 0.3.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 (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +112 -112
  3. package/package.json +83 -83
  4. package/server/bin/wild-workspace.mjs +995 -995
  5. package/server/src/account.mjs +114 -114
  6. package/server/src/agent-login.mjs +146 -146
  7. package/server/src/agent-readiness.mjs +200 -200
  8. package/server/src/agent.mjs +468 -453
  9. package/server/src/bazaar/core.mjs +579 -579
  10. package/server/src/bazaar/index.mjs +75 -75
  11. package/server/src/bazaar/mcp-server.mjs +328 -328
  12. package/server/src/bazaar/mock-tickup.mjs +97 -97
  13. package/server/src/bazaar/preview-server.mjs +95 -95
  14. package/server/src/bazaar/seed-recipes/customer-feedback-form/know-how.md +23 -23
  15. package/server/src/bazaar/seed-recipes/customer-feedback-form/recipe.json +24 -24
  16. package/server/src/bazaar/seed-recipes/landing-page-launch/know-how.md +29 -29
  17. package/server/src/bazaar/seed-recipes/landing-page-launch/recipe.json +25 -25
  18. package/server/src/bazaar/seed-recipes/personal-portfolio/know-how.md +21 -21
  19. package/server/src/bazaar/seed-recipes/personal-portfolio/recipe.json +24 -24
  20. package/server/src/bazaar/seed-recipes/receipt-sorter/know-how.md +31 -31
  21. package/server/src/bazaar/seed-recipes/receipt-sorter/recipe.json +25 -25
  22. package/server/src/bazaar/seed-recipes/tickup-hr-matching/know-how.md +79 -79
  23. package/server/src/bazaar/seed-recipes/tickup-hr-matching/recipe.json +32 -32
  24. package/server/src/canvas/core.mjs +421 -421
  25. package/server/src/canvas/index.mjs +42 -42
  26. package/server/src/canvas/mcp-server.mjs +253 -253
  27. package/server/src/config.mjs +404 -404
  28. package/server/src/daemon-bin.mjs +110 -110
  29. package/server/src/daemon-supervisor.mjs +285 -285
  30. package/server/src/doctor.mjs +375 -375
  31. package/server/src/inbox.mjs +86 -86
  32. package/server/src/index.mjs +2475 -2365
  33. package/server/src/logpaths.mjs +98 -98
  34. package/server/src/observability.mjs +45 -45
  35. package/server/src/operator.mjs +92 -92
  36. package/server/src/pairing.mjs +137 -137
  37. package/server/src/service.mjs +515 -515
  38. package/server/src/session-reporter.mjs +201 -201
  39. package/server/src/settings.mjs +145 -0
  40. package/server/src/share.mjs +182 -182
  41. package/server/src/skills.mjs +213 -0
  42. package/server/src/supervisor.mjs +647 -647
  43. package/server/src/support-consent.mjs +133 -133
  44. package/server/src/sync.mjs +248 -248
  45. package/server/src/transcript.mjs +121 -121
  46. package/server/src/turn-mcp.mjs +46 -46
  47. package/server/src/usage.mjs +405 -0
  48. package/web/dist/assets/index-BxRx8EsD.js +91 -0
  49. package/web/dist/assets/index-DoOPBr3s.css +1 -0
  50. package/web/dist/index.html +2 -2
  51. package/web/dist/assets/index-B7cOsWLt.js +0 -91
  52. package/web/dist/assets/index-Dl0VT5e6.css +0 -1
@@ -1,133 +1,133 @@
1
- // Support-channel consent (Phase 3, Pillar E) — the user's "VentureWild support
2
- // may act on my machine" grant for the daemon-hosted support channel (Pillar A).
3
- //
4
- // SECURITY POSTURE: OFF by default + PER-INCIDENT + TIME-BOXED (Tuan's locked
5
- // decisions, design doc Part 6). A grant is an explicit user gesture
6
- // (`wild-workspace support allow`), carries a tier (1 = read-only diagnostics/
7
- // logs; 2 = curated fixes) and an expiry, and auto-expires. Revoke is instant:
8
- // delete the file (`wild-workspace support revoke`).
9
- //
10
- // CRITICAL — lives in the machine-global dir (`~/.wild-workspace`), NOT the
11
- // per-workspace dataDir, because the **bmo-sync daemon** (a separate process) is
12
- // the enforcement gate and reads this exact file by the same `~/.wild-workspace`
13
- // convention. The relay never stores or checks consent — so a relay compromise
14
- // can't forge it; the daemon's local read is the hard gate. (Contrast
15
- // operator.mjs, which is server-read and keyed on dataDir.)
16
-
17
- import fs from 'node:fs';
18
- import path from 'node:path';
19
- import crypto from 'node:crypto';
20
-
21
- export const SUPPORT_CONSENT_VERSION = 1;
22
- // Tiers: 1 = read-only diagnostics/logs · 2 = curated fixes (restart/relink/
23
- // reinstall) · 3 = agent-mediated operation (Phase 4 — support drives the agent
24
- // on a task) · 4 = raw shell (the rare, loud escape hatch; Phase 4 PR 4.4).
25
- // Tiers 3–4 are "operate" grants and the CLI requires explicit confirmation.
26
- export const MAX_TIER = 4;
27
- /** At/above this tier a grant lets support OPERATE the machine (agent or shell). */
28
- export const OPERATE_TIER = 3;
29
- /** Cap a single grant so a forgotten "allow" can't leave the door open forever. */
30
- export const MAX_GRANT_MINUTES = 24 * 60;
31
-
32
- export function consentFile(globalDir) {
33
- return path.join(globalDir, 'support-consent.json');
34
- }
35
-
36
- /** The raw consent record, or null when absent/unparseable. No expiry check. */
37
- export function loadConsent(globalDir) {
38
- try {
39
- const p = JSON.parse(fs.readFileSync(consentFile(globalDir), 'utf8'));
40
- const tier = Number(p.tier);
41
- const expiresAt = Number(p.expiresAt);
42
- if (!Number.isFinite(tier) || !Number.isFinite(expiresAt)) return null;
43
- return {
44
- tier,
45
- grantedAt: Number(p.grantedAt) || null,
46
- expiresAt,
47
- nonce: typeof p.nonce === 'string' ? p.nonce : null,
48
- version: Number(p.version) || SUPPORT_CONSENT_VERSION,
49
- };
50
- } catch {
51
- return null;
52
- }
53
- }
54
-
55
- /**
56
- * Effective consent status, with the expiry applied. `enabled` is true only when
57
- * a grant exists AND hasn't expired. `now` is injectable for tests.
58
- */
59
- export function consentStatus(globalDir, { now = Date.now } = {}) {
60
- const rec = loadConsent(globalDir);
61
- const t = now();
62
- if (!rec) return { enabled: false, tier: 0, expiresAt: null, remainingMs: 0, file: consentFile(globalDir) };
63
- const remainingMs = Math.max(0, rec.expiresAt - t);
64
- const enabled = remainingMs > 0;
65
- return {
66
- enabled,
67
- tier: enabled ? rec.tier : 0,
68
- grantedAt: rec.grantedAt,
69
- expiresAt: rec.expiresAt,
70
- remainingMs,
71
- file: consentFile(globalDir),
72
- };
73
- }
74
-
75
- /**
76
- * Grant support consent at `tier` for `minutes`. Overwrites any prior grant (a
77
- * fresh, explicit gesture). Returns the stored record, or null if it couldn't be
78
- * persisted. `minutes` is clamped to (0, MAX_GRANT_MINUTES]; `tier` to [1, MAX_TIER].
79
- */
80
- export function grantConsent(globalDir, { tier = 1, minutes = 60, now = Date.now } = {}) {
81
- const clampedTier = Math.min(MAX_TIER, Math.max(1, Math.floor(Number(tier) || 1)));
82
- const clampedMinutes = Math.min(MAX_GRANT_MINUTES, Math.max(1, Math.floor(Number(minutes) || 1)));
83
- const grantedAt = now();
84
- const rec = {
85
- tier: clampedTier,
86
- grantedAt,
87
- expiresAt: grantedAt + clampedMinutes * 60_000,
88
- nonce: crypto.randomBytes(12).toString('base64url'),
89
- version: SUPPORT_CONSENT_VERSION,
90
- };
91
- try {
92
- fs.mkdirSync(globalDir, { recursive: true });
93
- fs.writeFileSync(consentFile(globalDir), JSON.stringify(rec, null, 2), { mode: 0o600 });
94
- } catch {
95
- return null;
96
- }
97
- return rec;
98
- }
99
-
100
- /** Revoke instantly by deleting the file. Returns true if a grant was removed. */
101
- export function revokeConsent(globalDir) {
102
- try {
103
- fs.rmSync(consentFile(globalDir));
104
- return true;
105
- } catch {
106
- return false;
107
- }
108
- }
109
-
110
- export function auditFile(globalDir) {
111
- return path.join(globalDir, 'support-audit.jsonl');
112
- }
113
-
114
- /**
115
- * The user-owned support audit: the most recent `limit` actions the daemon
116
- * recorded (newest first). Written by the bmo-sync daemon (one JSON object per
117
- * line) into the same machine-global dir; this is the read side the UI/CLI shows.
118
- * Best-effort — a missing/corrupt file yields an empty list.
119
- */
120
- export function readAudit(globalDir, { limit = 50 } = {}) {
121
- let raw;
122
- try {
123
- raw = fs.readFileSync(auditFile(globalDir), 'utf8');
124
- } catch {
125
- return [];
126
- }
127
- const lines = raw.split('\n').filter(Boolean);
128
- const out = [];
129
- for (const line of lines.slice(-limit)) {
130
- try { out.push(JSON.parse(line)); } catch { /* skip a partial/corrupt line */ }
131
- }
132
- return out.reverse(); // newest first
133
- }
1
+ // Support-channel consent (Phase 3, Pillar E) — the user's "VentureWild support
2
+ // may act on my machine" grant for the daemon-hosted support channel (Pillar A).
3
+ //
4
+ // SECURITY POSTURE: OFF by default + PER-INCIDENT + TIME-BOXED (Tuan's locked
5
+ // decisions, design doc Part 6). A grant is an explicit user gesture
6
+ // (`wild-workspace support allow`), carries a tier (1 = read-only diagnostics/
7
+ // logs; 2 = curated fixes) and an expiry, and auto-expires. Revoke is instant:
8
+ // delete the file (`wild-workspace support revoke`).
9
+ //
10
+ // CRITICAL — lives in the machine-global dir (`~/.wild-workspace`), NOT the
11
+ // per-workspace dataDir, because the **bmo-sync daemon** (a separate process) is
12
+ // the enforcement gate and reads this exact file by the same `~/.wild-workspace`
13
+ // convention. The relay never stores or checks consent — so a relay compromise
14
+ // can't forge it; the daemon's local read is the hard gate. (Contrast
15
+ // operator.mjs, which is server-read and keyed on dataDir.)
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import crypto from 'node:crypto';
20
+
21
+ export const SUPPORT_CONSENT_VERSION = 1;
22
+ // Tiers: 1 = read-only diagnostics/logs · 2 = curated fixes (restart/relink/
23
+ // reinstall) · 3 = agent-mediated operation (Phase 4 — support drives the agent
24
+ // on a task) · 4 = raw shell (the rare, loud escape hatch; Phase 4 PR 4.4).
25
+ // Tiers 3–4 are "operate" grants and the CLI requires explicit confirmation.
26
+ export const MAX_TIER = 4;
27
+ /** At/above this tier a grant lets support OPERATE the machine (agent or shell). */
28
+ export const OPERATE_TIER = 3;
29
+ /** Cap a single grant so a forgotten "allow" can't leave the door open forever. */
30
+ export const MAX_GRANT_MINUTES = 24 * 60;
31
+
32
+ export function consentFile(globalDir) {
33
+ return path.join(globalDir, 'support-consent.json');
34
+ }
35
+
36
+ /** The raw consent record, or null when absent/unparseable. No expiry check. */
37
+ export function loadConsent(globalDir) {
38
+ try {
39
+ const p = JSON.parse(fs.readFileSync(consentFile(globalDir), 'utf8'));
40
+ const tier = Number(p.tier);
41
+ const expiresAt = Number(p.expiresAt);
42
+ if (!Number.isFinite(tier) || !Number.isFinite(expiresAt)) return null;
43
+ return {
44
+ tier,
45
+ grantedAt: Number(p.grantedAt) || null,
46
+ expiresAt,
47
+ nonce: typeof p.nonce === 'string' ? p.nonce : null,
48
+ version: Number(p.version) || SUPPORT_CONSENT_VERSION,
49
+ };
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Effective consent status, with the expiry applied. `enabled` is true only when
57
+ * a grant exists AND hasn't expired. `now` is injectable for tests.
58
+ */
59
+ export function consentStatus(globalDir, { now = Date.now } = {}) {
60
+ const rec = loadConsent(globalDir);
61
+ const t = now();
62
+ if (!rec) return { enabled: false, tier: 0, expiresAt: null, remainingMs: 0, file: consentFile(globalDir) };
63
+ const remainingMs = Math.max(0, rec.expiresAt - t);
64
+ const enabled = remainingMs > 0;
65
+ return {
66
+ enabled,
67
+ tier: enabled ? rec.tier : 0,
68
+ grantedAt: rec.grantedAt,
69
+ expiresAt: rec.expiresAt,
70
+ remainingMs,
71
+ file: consentFile(globalDir),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Grant support consent at `tier` for `minutes`. Overwrites any prior grant (a
77
+ * fresh, explicit gesture). Returns the stored record, or null if it couldn't be
78
+ * persisted. `minutes` is clamped to (0, MAX_GRANT_MINUTES]; `tier` to [1, MAX_TIER].
79
+ */
80
+ export function grantConsent(globalDir, { tier = 1, minutes = 60, now = Date.now } = {}) {
81
+ const clampedTier = Math.min(MAX_TIER, Math.max(1, Math.floor(Number(tier) || 1)));
82
+ const clampedMinutes = Math.min(MAX_GRANT_MINUTES, Math.max(1, Math.floor(Number(minutes) || 1)));
83
+ const grantedAt = now();
84
+ const rec = {
85
+ tier: clampedTier,
86
+ grantedAt,
87
+ expiresAt: grantedAt + clampedMinutes * 60_000,
88
+ nonce: crypto.randomBytes(12).toString('base64url'),
89
+ version: SUPPORT_CONSENT_VERSION,
90
+ };
91
+ try {
92
+ fs.mkdirSync(globalDir, { recursive: true });
93
+ fs.writeFileSync(consentFile(globalDir), JSON.stringify(rec, null, 2), { mode: 0o600 });
94
+ } catch {
95
+ return null;
96
+ }
97
+ return rec;
98
+ }
99
+
100
+ /** Revoke instantly by deleting the file. Returns true if a grant was removed. */
101
+ export function revokeConsent(globalDir) {
102
+ try {
103
+ fs.rmSync(consentFile(globalDir));
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ export function auditFile(globalDir) {
111
+ return path.join(globalDir, 'support-audit.jsonl');
112
+ }
113
+
114
+ /**
115
+ * The user-owned support audit: the most recent `limit` actions the daemon
116
+ * recorded (newest first). Written by the bmo-sync daemon (one JSON object per
117
+ * line) into the same machine-global dir; this is the read side the UI/CLI shows.
118
+ * Best-effort — a missing/corrupt file yields an empty list.
119
+ */
120
+ export function readAudit(globalDir, { limit = 50 } = {}) {
121
+ let raw;
122
+ try {
123
+ raw = fs.readFileSync(auditFile(globalDir), 'utf8');
124
+ } catch {
125
+ return [];
126
+ }
127
+ const lines = raw.split('\n').filter(Boolean);
128
+ const out = [];
129
+ for (const line of lines.slice(-limit)) {
130
+ try { out.push(JSON.parse(line)); } catch { /* skip a partial/corrupt line */ }
131
+ }
132
+ return out.reverse(); // newest first
133
+ }