@superjackfruitlabs/salaam 0.0.3 → 0.0.4

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 (102) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +12 -11
  3. package/dist/src/acp-outcome.js +177 -17
  4. package/dist/src/acp.js +41 -7
  5. package/dist/src/agent-profiles.js +45 -0
  6. package/dist/src/agentpod-authority.js +169 -11
  7. package/dist/src/agentpod-default-config.js +12 -0
  8. package/dist/src/agentpod-work-decision-protocol.js +40 -6
  9. package/dist/src/agentpod-work-decisions.js +108 -4
  10. package/dist/src/agentpod-work.js +1214 -127
  11. package/dist/src/board-reads.js +106 -0
  12. package/dist/src/children.js +83 -0
  13. package/dist/src/cli-help.js +27 -2
  14. package/dist/src/cli.js +87 -3
  15. package/dist/src/command-network.js +200 -0
  16. package/dist/src/command-sandbox.js +217 -0
  17. package/dist/src/execution-evidence.js +33 -1
  18. package/dist/src/host-bind.js +111 -0
  19. package/dist/src/host-breaker.js +88 -0
  20. package/dist/src/host-start.js +41 -1
  21. package/dist/src/host.js +192 -6
  22. package/dist/src/house-extension.js +162 -57
  23. package/dist/src/local-coding.js +214 -18
  24. package/dist/src/local-edit-helper.js +25 -0
  25. package/dist/src/local-undo.js +25 -16
  26. package/dist/src/managed.js +1 -1
  27. package/dist/src/operator-mcp-cli.js +107 -0
  28. package/dist/src/operator-mcp.js +342 -0
  29. package/dist/src/persona.js +31 -0
  30. package/dist/src/pi.js +22 -1
  31. package/dist/src/protocol.js +5 -2
  32. package/dist/src/rate-limit-retries.js +62 -0
  33. package/dist/src/recurring-work.js +1 -0
  34. package/dist/src/repository-snapshots.js +5 -2
  35. package/dist/src/run-grants.js +62 -0
  36. package/dist/src/service.js +141 -6
  37. package/dist/src/station-checkpoints.js +139 -0
  38. package/dist/src/station-config.js +8 -1
  39. package/dist/src/station-files.js +20 -1
  40. package/dist/src/station-task-verification.js +6 -3
  41. package/dist/src/station-work.js +315 -22
  42. package/dist/src/store.js +19 -0
  43. package/dist/src/supervisor.js +177 -5
  44. package/dist/src/task-budget.js +39 -3
  45. package/dist/src/task-critic.js +115 -0
  46. package/dist/src/task-driver.js +46 -2
  47. package/dist/src/task-policy.js +44 -12
  48. package/dist/src/task-verification.js +42 -9
  49. package/dist/src/task-work.js +47 -0
  50. package/dist/src/tool-identity.js +69 -0
  51. package/dist/src/usage.js +1 -1
  52. package/npm-shrinkwrap.json +2 -2
  53. package/package.json +1 -1
  54. package/src/acp-outcome.ts +169 -19
  55. package/src/acp.ts +53 -7
  56. package/src/agent-profiles.ts +48 -0
  57. package/src/agentpod-authority.ts +234 -15
  58. package/src/agentpod-default-config.ts +16 -0
  59. package/src/agentpod-work-decision-protocol.ts +69 -8
  60. package/src/agentpod-work-decisions.ts +123 -5
  61. package/src/agentpod-work.ts +1455 -152
  62. package/src/board-reads.ts +148 -0
  63. package/src/children.ts +112 -0
  64. package/src/cli-help.ts +27 -2
  65. package/src/cli.ts +109 -3
  66. package/src/command-network.ts +215 -0
  67. package/src/command-sandbox.ts +273 -0
  68. package/src/execution-evidence.ts +31 -1
  69. package/src/host-bind.ts +156 -0
  70. package/src/host-breaker.ts +89 -0
  71. package/src/host-start.ts +51 -1
  72. package/src/host.ts +244 -6
  73. package/src/house-extension.ts +174 -56
  74. package/src/local-coding.ts +270 -18
  75. package/src/local-edit-helper.ts +26 -0
  76. package/src/local-undo.ts +29 -14
  77. package/src/managed.ts +2 -2
  78. package/src/operator-mcp-cli.ts +108 -0
  79. package/src/operator-mcp.ts +420 -0
  80. package/src/persona.ts +37 -0
  81. package/src/pi.ts +23 -1
  82. package/src/protocol.ts +6 -1
  83. package/src/rate-limit-retries.ts +69 -0
  84. package/src/recurring-work.ts +1 -0
  85. package/src/repository-snapshots.ts +5 -2
  86. package/src/run-grants.ts +107 -0
  87. package/src/service.ts +222 -6
  88. package/src/station-checkpoints.ts +183 -0
  89. package/src/station-config.ts +10 -1
  90. package/src/station-files.ts +20 -1
  91. package/src/station-task-verification.ts +6 -3
  92. package/src/station-work.ts +369 -20
  93. package/src/store.ts +18 -0
  94. package/src/supervisor.ts +203 -4
  95. package/src/task-budget.ts +48 -3
  96. package/src/task-critic.ts +171 -0
  97. package/src/task-driver.ts +49 -2
  98. package/src/task-policy.ts +65 -13
  99. package/src/task-verification.ts +53 -9
  100. package/src/task-work.ts +75 -0
  101. package/src/tool-identity.ts +110 -0
  102. package/src/usage.ts +1 -1
@@ -2,14 +2,43 @@ import { randomUUID } from 'node:crypto';
2
2
  import type { AgentPodWork } from './agentpod-work.js';
3
3
  import type { ManagedWork } from './managed.js';
4
4
  import { canonical, digest, ServiceError } from './protocol.js';
5
- import { workDecisionAnswer, type WorkDecision } from './agentpod-work-decision-protocol.js';
5
+ import { type RunGrants, runGrantCovers, runGrantPrefix } from './run-grants.js';
6
+ import {
7
+ workDecisionAnswer,
8
+ type WorkDecision,
9
+ type WorkDecisionDetail,
10
+ } from './agentpod-work-decision-protocol.js';
6
11
  /** Private authenticated Hub control only. Answers choose an existing approval;
7
12
  * they never execute an operation or supply a second effect grant. */
8
13
  export class AgentPodWorkDecisions {
9
14
  constructor(
10
15
  readonly work: AgentPodWork,
11
16
  readonly managed: ManagedWork,
17
+ readonly grants?: RunGrants,
12
18
  ) {}
19
+ /** The run directory a grant for this operation would cover, when it is a covered write. */
20
+ private grantable(binding: unknown, op: { kind: string; value: string }): string | null {
21
+ if (!this.grants || op.kind !== 'workspace') return null;
22
+ const run = (binding as { run?: { id?: unknown } } | null)?.run;
23
+ const prefix = runGrantPrefix(typeof run?.id === 'string' ? run.id : '');
24
+ let args: Record<string, unknown> = {};
25
+ try {
26
+ args = JSON.parse(op.value) as Record<string, unknown>;
27
+ } catch {
28
+ return null;
29
+ }
30
+ return prefix && runGrantCovers(prefix, args) ? prefix : null;
31
+ }
32
+ /** "Always allow" covers exactly what the standing workspace-write rule answers: a workspace
33
+ * write anywhere in the workspace, never a delete or a hidden path. */
34
+ private alwaysAllowable(op: { kind: string; value: string }): boolean {
35
+ if (op.kind !== 'workspace') return false;
36
+ try {
37
+ return runGrantCovers('', JSON.parse(op.value) as Record<string, unknown>);
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
13
42
  private owner(binding: unknown) {
14
43
  const root = this.work.authority.inspect(binding);
15
44
  const row = this.work.store.db
@@ -51,10 +80,23 @@ export class AgentPodWorkDecisions {
51
80
  version: 1,
52
81
  request_id: randomUUID(),
53
82
  ...identity,
54
- question: 'Approve this exact operation? ' + op.kind + ' ' + op.action_key,
83
+ question: approvalDetail(op).summary + '?',
84
+ detail: approvalDetail(op),
55
85
  options: [
56
- { id: 'approve', title: 'Approve once' },
57
- { id: 'deny', title: 'Deny' },
86
+ { id: 'approve' as const, title: 'Approve once' },
87
+ ...(this.grantable(root.binding, op)
88
+ ? [{ id: 'approve-run' as const, title: 'Approve for this run' }]
89
+ : []),
90
+ ...(this.alwaysAllowable(op)
91
+ ? [
92
+ {
93
+ id: 'always' as const,
94
+ title: 'Always allow this agent to write in its workspace',
95
+ requires: 'workspace-admin' as const,
96
+ },
97
+ ]
98
+ : []),
99
+ { id: 'deny' as const, title: 'Deny' },
58
100
  ],
59
101
  }),
60
102
  );
@@ -97,13 +139,45 @@ export class AgentPodWorkDecisions {
97
139
  approval.expires_at !== offered.expires_at
98
140
  )
99
141
  throw new ServiceError('approval_mismatch', 'Approval ownership changed');
142
+ // "Approve for this run" must have been offered, and still be grantable, to be chosen.
143
+ const prefix =
144
+ answer.option_id === 'approve-run'
145
+ ? this.grantable(root.binding, this.managed.operation(offered.operation_id))
146
+ : null;
147
+ if (
148
+ answer.option_id === 'approve-run' &&
149
+ (!prefix || !offered.options.some((o) => o.id === 'approve-run'))
150
+ )
151
+ throw new ServiceError('approval_mismatch', 'This approval cannot cover the run');
152
+ // "Always allow" approves this write; the standing rule is the hub's, set before it forwards.
153
+ if (
154
+ answer.option_id === 'always' &&
155
+ (!offered.options.some((o) => o.id === 'always') ||
156
+ !this.alwaysAllowable(this.managed.operation(offered.operation_id)))
157
+ )
158
+ throw new ServiceError(
159
+ 'approval_mismatch',
160
+ 'Always allow was not offered for this write',
161
+ );
100
162
  this.managed.decide(
101
163
  offered.approval_id,
102
164
  answer.request_id,
103
165
  offered.approval_revision,
104
166
  offered.operation_digest,
105
- answer.option_id,
167
+ answer.option_id === 'deny' ? 'deny' : 'approve',
106
168
  );
169
+ const grant =
170
+ answer.option_id === 'approve-run'
171
+ ? this.grants!.createInTransaction({
172
+ session_id: root.session_id,
173
+ task_id: task.id,
174
+ plan_revision: offered.plan_revision,
175
+ binding_digest: offered.binding_digest,
176
+ path_prefix: prefix!,
177
+ approval_id: offered.approval_id,
178
+ granted_by: answer.answered_by,
179
+ })
180
+ : null;
107
181
  this.work.store.execution.assert(root.session_id);
108
182
  return {
109
183
  version: 1 as const,
@@ -111,8 +185,52 @@ export class AgentPodWorkDecisions {
111
185
  decision_digest: answer.decision_digest,
112
186
  approval_id: offered.approval_id,
113
187
  decision: answer.option_id,
188
+ ...(grant ? { grant_id: grant.id } : {}),
114
189
  };
115
190
  },
116
191
  );
117
192
  }
118
193
  }
194
+
195
+ /** Readable approvals: what a person approves, from the operation's own canonical arguments only.
196
+ * A workspace write names its file, the change, its size, a short preview and the agent's one-line
197
+ * reason; anything else names its kind and target. The digest still binds the exact operation. */
198
+ export function approvalDetail(op: {
199
+ kind: string;
200
+ action_key: string;
201
+ target: string;
202
+ value: string;
203
+ operation_digest: string;
204
+ }): WorkDecisionDetail {
205
+ const base = { version: 1 as const, reason: null, files: [], digest: op.operation_digest };
206
+ if (op.kind === 'review') return { ...base, summary: 'Accept this step for review' };
207
+ if (op.kind !== 'workspace')
208
+ return { ...base, summary: ('Run ' + op.kind + ' ' + op.target).slice(0, 200) };
209
+ let args: Record<string, unknown> = {};
210
+ try {
211
+ args = JSON.parse(op.value) as Record<string, unknown>;
212
+ } catch {
213
+ /* not a patch: the generic form below */
214
+ }
215
+ const path = typeof args.path === 'string' ? args.path : op.target.replace(/^station:/, '');
216
+ const content = typeof args.content === 'string' ? args.content : null;
217
+ const change = args.content === null ? 'delete' : args.expected_hash === null ? 'create' : 'edit';
218
+ const verb = { create: 'Create', edit: 'Edit', delete: 'Delete' }[change];
219
+ const reason =
220
+ typeof args.reason === 'string' && args.reason.trim()
221
+ ? args.reason.replace(/\s+/g, ' ').trim().slice(0, 300)
222
+ : null;
223
+ return {
224
+ ...base,
225
+ summary: (verb + ' ' + path + ' in the workspace').slice(0, 200),
226
+ reason,
227
+ files: [
228
+ {
229
+ path: path.slice(0, 512),
230
+ change,
231
+ bytes: content === null ? null : Buffer.byteLength(content),
232
+ preview: content === null ? null : content.slice(0, 1200),
233
+ },
234
+ ],
235
+ };
236
+ }