agent-relay 4.0.2 → 4.0.3

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 (175) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +7906 -2084
  6. package/dist/packages/sdk/src/provisioner/seeder.d.ts +17 -0
  7. package/dist/packages/sdk/src/provisioner/seeder.d.ts.map +1 -0
  8. package/dist/packages/sdk/src/provisioner/seeder.js +419 -0
  9. package/dist/packages/sdk/src/provisioner/seeder.js.map +1 -0
  10. package/dist/packages/sdk/src/provisioner/token.d.ts +38 -0
  11. package/dist/packages/sdk/src/provisioner/token.d.ts.map +1 -0
  12. package/dist/packages/sdk/src/provisioner/token.js +74 -0
  13. package/dist/packages/sdk/src/provisioner/token.js.map +1 -0
  14. package/dist/src/cli/commands/core.d.ts.map +1 -1
  15. package/dist/src/cli/commands/core.js +7 -3
  16. package/dist/src/cli/commands/core.js.map +1 -1
  17. package/dist/src/cli/commands/on/provision.d.ts.map +1 -1
  18. package/dist/src/cli/commands/on/provision.js +8 -3
  19. package/dist/src/cli/commands/on/provision.js.map +1 -1
  20. package/dist/src/cli/commands/on/start.d.ts +3 -0
  21. package/dist/src/cli/commands/on/start.d.ts.map +1 -1
  22. package/dist/src/cli/commands/on/start.js +113 -84
  23. package/dist/src/cli/commands/on/start.js.map +1 -1
  24. package/dist/src/cli/commands/on/symlink-mount.d.ts +12 -0
  25. package/dist/src/cli/commands/on/symlink-mount.d.ts.map +1 -0
  26. package/dist/src/cli/commands/on/symlink-mount.js +304 -0
  27. package/dist/src/cli/commands/on/symlink-mount.js.map +1 -0
  28. package/dist/src/cli/commands/on.d.ts.map +1 -1
  29. package/dist/src/cli/commands/on.js +3 -0
  30. package/dist/src/cli/commands/on.js.map +1 -1
  31. package/install.sh +4 -0
  32. package/package.json +9 -9
  33. package/packages/acp-bridge/package.json +2 -2
  34. package/packages/brand/package.json +1 -1
  35. package/packages/cloud/package.json +2 -2
  36. package/packages/config/package.json +1 -1
  37. package/packages/hooks/package.json +4 -4
  38. package/packages/memory/package.json +2 -2
  39. package/packages/openclaw/package.json +2 -2
  40. package/packages/policy/package.json +2 -2
  41. package/packages/sdk/dist/client.d.ts +3 -10
  42. package/packages/sdk/dist/client.d.ts.map +1 -1
  43. package/packages/sdk/dist/client.js +2 -0
  44. package/packages/sdk/dist/client.js.map +1 -1
  45. package/packages/sdk/dist/provisioner/__tests__/audit.test.d.ts +2 -0
  46. package/packages/sdk/dist/provisioner/__tests__/audit.test.d.ts.map +1 -0
  47. package/packages/sdk/dist/provisioner/__tests__/audit.test.js +45 -0
  48. package/packages/sdk/dist/provisioner/__tests__/audit.test.js.map +1 -0
  49. package/packages/sdk/dist/provisioner/__tests__/compiler.test.d.ts +2 -0
  50. package/packages/sdk/dist/provisioner/__tests__/compiler.test.d.ts.map +1 -0
  51. package/packages/sdk/dist/provisioner/__tests__/compiler.test.js +345 -0
  52. package/packages/sdk/dist/provisioner/__tests__/compiler.test.js.map +1 -0
  53. package/packages/sdk/dist/provisioner/__tests__/presets.test.d.ts +2 -0
  54. package/packages/sdk/dist/provisioner/__tests__/presets.test.d.ts.map +1 -0
  55. package/packages/sdk/dist/provisioner/__tests__/presets.test.js +23 -0
  56. package/packages/sdk/dist/provisioner/__tests__/presets.test.js.map +1 -0
  57. package/packages/sdk/dist/provisioner/__tests__/seeder.test.d.ts +2 -0
  58. package/packages/sdk/dist/provisioner/__tests__/seeder.test.d.ts.map +1 -0
  59. package/packages/sdk/dist/provisioner/__tests__/seeder.test.js +224 -0
  60. package/packages/sdk/dist/provisioner/__tests__/seeder.test.js.map +1 -0
  61. package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.d.ts +2 -0
  62. package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.d.ts.map +1 -0
  63. package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.js +191 -0
  64. package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.js.map +1 -0
  65. package/packages/sdk/dist/provisioner/__tests__/token-factory.test.d.ts +2 -0
  66. package/packages/sdk/dist/provisioner/__tests__/token-factory.test.d.ts.map +1 -0
  67. package/packages/sdk/dist/provisioner/__tests__/token-factory.test.js +127 -0
  68. package/packages/sdk/dist/provisioner/__tests__/token-factory.test.js.map +1 -0
  69. package/packages/sdk/dist/provisioner/__tests__/token.test.d.ts +2 -0
  70. package/packages/sdk/dist/provisioner/__tests__/token.test.d.ts.map +1 -0
  71. package/packages/sdk/dist/provisioner/__tests__/token.test.js +44 -0
  72. package/packages/sdk/dist/provisioner/__tests__/token.test.js.map +1 -0
  73. package/packages/sdk/dist/provisioner/audit.d.ts +19 -0
  74. package/packages/sdk/dist/provisioner/audit.d.ts.map +1 -0
  75. package/packages/sdk/dist/provisioner/audit.js +74 -0
  76. package/packages/sdk/dist/provisioner/audit.js.map +1 -0
  77. package/packages/sdk/dist/provisioner/compiler.d.ts +23 -0
  78. package/packages/sdk/dist/provisioner/compiler.d.ts.map +1 -0
  79. package/packages/sdk/dist/provisioner/compiler.js +355 -0
  80. package/packages/sdk/dist/provisioner/compiler.js.map +1 -0
  81. package/packages/sdk/dist/provisioner/index.d.ts +9 -0
  82. package/packages/sdk/dist/provisioner/index.d.ts.map +1 -0
  83. package/packages/sdk/dist/provisioner/index.js +266 -0
  84. package/packages/sdk/dist/provisioner/index.js.map +1 -0
  85. package/packages/sdk/dist/provisioner/mount.d.ts +14 -0
  86. package/packages/sdk/dist/provisioner/mount.d.ts.map +1 -0
  87. package/packages/sdk/dist/provisioner/mount.js +329 -0
  88. package/packages/sdk/dist/provisioner/mount.js.map +1 -0
  89. package/packages/sdk/dist/provisioner/seeder.d.ts +17 -0
  90. package/packages/sdk/dist/provisioner/seeder.d.ts.map +1 -0
  91. package/packages/sdk/dist/provisioner/seeder.js +419 -0
  92. package/packages/sdk/dist/provisioner/seeder.js.map +1 -0
  93. package/packages/sdk/dist/provisioner/token.d.ts +38 -0
  94. package/packages/sdk/dist/provisioner/token.d.ts.map +1 -0
  95. package/packages/sdk/dist/provisioner/token.js +74 -0
  96. package/packages/sdk/dist/provisioner/token.js.map +1 -0
  97. package/packages/sdk/dist/provisioner/types.d.ts +133 -0
  98. package/packages/sdk/dist/provisioner/types.d.ts.map +1 -0
  99. package/packages/sdk/dist/provisioner/types.js +2 -0
  100. package/packages/sdk/dist/provisioner/types.js.map +1 -0
  101. package/packages/sdk/dist/relay.d.ts +6 -0
  102. package/packages/sdk/dist/relay.d.ts.map +1 -1
  103. package/packages/sdk/dist/relay.js +17 -5
  104. package/packages/sdk/dist/relay.js.map +1 -1
  105. package/packages/sdk/dist/types.d.ts +9 -0
  106. package/packages/sdk/dist/types.d.ts.map +1 -1
  107. package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.d.ts +2 -0
  108. package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.d.ts.map +1 -0
  109. package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.js +331 -0
  110. package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.js.map +1 -0
  111. package/packages/sdk/dist/workflows/__tests__/permission-types.test.d.ts +2 -0
  112. package/packages/sdk/dist/workflows/__tests__/permission-types.test.d.ts.map +1 -0
  113. package/packages/sdk/dist/workflows/__tests__/permission-types.test.js +124 -0
  114. package/packages/sdk/dist/workflows/__tests__/permission-types.test.js.map +1 -0
  115. package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.d.ts +2 -0
  116. package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.d.ts.map +1 -0
  117. package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.js +526 -0
  118. package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.js.map +1 -0
  119. package/packages/sdk/dist/workflows/dry-run-format.d.ts.map +1 -1
  120. package/packages/sdk/dist/workflows/dry-run-format.js +8 -0
  121. package/packages/sdk/dist/workflows/dry-run-format.js.map +1 -1
  122. package/packages/sdk/dist/workflows/runner.d.ts +14 -0
  123. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  124. package/packages/sdk/dist/workflows/runner.js +455 -6
  125. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  126. package/packages/sdk/dist/workflows/types.d.ts +190 -0
  127. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  128. package/packages/sdk/dist/workflows/types.js +29 -0
  129. package/packages/sdk/dist/workflows/types.js.map +1 -1
  130. package/packages/sdk/package.json +6 -2
  131. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +123 -1
  132. package/packages/sdk/src/__tests__/provisioner-mount.test.ts +126 -0
  133. package/packages/sdk/src/__tests__/spawn-token.test.ts +41 -0
  134. package/packages/sdk/src/__tests__/workflow-runner.test.ts +77 -45
  135. package/packages/sdk/src/client.ts +4 -8
  136. package/packages/sdk/src/provisioner/__tests__/audit.test.ts +62 -0
  137. package/packages/sdk/src/provisioner/__tests__/compiler.test.ts +369 -0
  138. package/packages/sdk/src/provisioner/__tests__/presets.test.ts +25 -0
  139. package/packages/sdk/src/provisioner/__tests__/seeder.test.ts +284 -0
  140. package/packages/sdk/src/provisioner/__tests__/tar-seeder.test.ts +249 -0
  141. package/packages/sdk/src/provisioner/__tests__/token-factory.test.ts +172 -0
  142. package/packages/sdk/src/provisioner/__tests__/token.test.ts +53 -0
  143. package/packages/sdk/src/provisioner/audit.ts +104 -0
  144. package/packages/sdk/src/provisioner/compiler.ts +498 -0
  145. package/packages/sdk/src/provisioner/index.ts +332 -0
  146. package/packages/sdk/src/provisioner/mount.ts +419 -0
  147. package/packages/sdk/src/provisioner/seeder.ts +571 -0
  148. package/packages/sdk/src/provisioner/token.ts +112 -0
  149. package/packages/sdk/src/provisioner/types.ts +188 -0
  150. package/packages/sdk/src/relay.ts +31 -9
  151. package/packages/sdk/src/types.ts +9 -0
  152. package/packages/sdk/src/workflows/__tests__/e2e-permissions.test.ts +407 -0
  153. package/packages/sdk/src/workflows/__tests__/fixtures/.agentignore +2 -0
  154. package/packages/sdk/src/workflows/__tests__/fixtures/.reader.agentreadonly +2 -0
  155. package/packages/sdk/src/workflows/__tests__/fixtures/permission-test.yaml +42 -0
  156. package/packages/sdk/src/workflows/__tests__/permission-types.test.ts +154 -0
  157. package/packages/sdk/src/workflows/__tests__/permissions-integration.test.ts +649 -0
  158. package/packages/sdk/src/workflows/builtin-templates/bug-fix.yaml +13 -9
  159. package/packages/sdk/src/workflows/builtin-templates/code-review.yaml +12 -8
  160. package/packages/sdk/src/workflows/builtin-templates/competitive.yaml +11 -7
  161. package/packages/sdk/src/workflows/builtin-templates/documentation.yaml +16 -8
  162. package/packages/sdk/src/workflows/builtin-templates/feature-dev.yaml +13 -9
  163. package/packages/sdk/src/workflows/builtin-templates/refactor.yaml +13 -9
  164. package/packages/sdk/src/workflows/builtin-templates/review-loop.yaml +14 -10
  165. package/packages/sdk/src/workflows/builtin-templates/security-audit.yaml +19 -9
  166. package/packages/sdk/src/workflows/dry-run-format.ts +14 -1
  167. package/packages/sdk/src/workflows/runner.ts +559 -6
  168. package/packages/sdk/src/workflows/schema.json +204 -114
  169. package/packages/sdk/src/workflows/types.ts +266 -1
  170. package/packages/sdk/vitest.config.ts +5 -1
  171. package/packages/sdk-py/pyproject.toml +1 -1
  172. package/packages/telemetry/package.json +1 -1
  173. package/packages/trajectory/package.json +2 -2
  174. package/packages/user-directory/package.json +2 -2
  175. package/packages/utils/package.json +2 -2
@@ -0,0 +1,172 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createHmac } from 'node:crypto';
3
+ import test from 'node:test';
4
+
5
+ import {
6
+ DEFAULT_ADMIN_AGENT_NAME,
7
+ DEFAULT_ADMIN_SCOPES,
8
+ DEFAULT_WORKFLOW_TOKEN_TTL_SECONDS,
9
+ WorkflowTokenFactory,
10
+ mintAgentToken,
11
+ type TokenClaims,
12
+ } from '../token.js';
13
+
14
+ interface JwtHeader {
15
+ alg: string;
16
+ typ: string;
17
+ }
18
+
19
+ function decodeJwtPart<T>(value: string): T {
20
+ return JSON.parse(Buffer.from(value, 'base64url').toString('utf8')) as T;
21
+ }
22
+
23
+ function decodeJwt(token: string): { header: JwtHeader; payload: TokenClaims; signature: string } {
24
+ const [header, payload, signature] = token.split('.');
25
+ assert.ok(header);
26
+ assert.ok(payload);
27
+ assert.ok(signature);
28
+
29
+ return {
30
+ header: decodeJwtPart<JwtHeader>(header),
31
+ payload: decodeJwtPart<TokenClaims>(payload),
32
+ signature,
33
+ };
34
+ }
35
+
36
+ test('mintAgentToken returns a valid JWT', () => {
37
+ const token = mintAgentToken({
38
+ secret: 'test-secret',
39
+ agentName: 'worker',
40
+ workspace: 'workspace-123',
41
+ scopes: ['relayfile:fs:read:/src/index.ts'],
42
+ });
43
+
44
+ const parts = token.split('.');
45
+ const decoded = decodeJwt(token);
46
+
47
+ assert.equal(parts.length, 3);
48
+ assert.ok(parts.every((part) => /^[A-Za-z0-9_-]+$/u.test(part)));
49
+ assert.deepEqual(decoded.header, { alg: 'HS256', typ: 'JWT' });
50
+ assert.equal(decoded.payload.sub, 'agent_worker');
51
+ });
52
+
53
+ test('mintAgentToken payload contains agent_name, workspace, and scopes', () => {
54
+ const scopes = ['relayfile:fs:read:/src/index.ts', 'relayfile:fs:write:/src/index.ts'];
55
+ const token = mintAgentToken({
56
+ secret: 'test-secret',
57
+ agentName: 'compiler',
58
+ workspace: 'workspace-abc',
59
+ scopes,
60
+ });
61
+
62
+ const { payload } = decodeJwt(token);
63
+
64
+ assert.equal(payload.agent_name, 'compiler');
65
+ assert.equal(payload.wks, 'workspace-abc');
66
+ assert.equal(payload.workspace_id, 'workspace-abc');
67
+ assert.deepEqual(payload.scopes, scopes);
68
+ });
69
+
70
+ test('mintAgentToken defaults expiry to 2 hours', () => {
71
+ const token = mintAgentToken({
72
+ secret: 'test-secret',
73
+ agentName: 'worker',
74
+ workspace: 'workspace-123',
75
+ scopes: [],
76
+ });
77
+
78
+ const { payload } = decodeJwt(token);
79
+
80
+ assert.equal(payload.exp - payload.iat, DEFAULT_WORKFLOW_TOKEN_TTL_SECONDS);
81
+ assert.equal(payload.exp - payload.iat, 2 * 60 * 60);
82
+ });
83
+
84
+ test('mintAgentToken applies a custom TTL', () => {
85
+ const token = mintAgentToken({
86
+ secret: 'test-secret',
87
+ agentName: 'worker',
88
+ workspace: 'workspace-123',
89
+ scopes: [],
90
+ ttlSeconds: 90,
91
+ });
92
+
93
+ const { payload } = decodeJwt(token);
94
+
95
+ assert.equal(payload.exp - payload.iat, 90);
96
+ });
97
+
98
+ test('WorkflowTokenFactory mintAdmin uses the default admin identity and scopes', () => {
99
+ const factory = new WorkflowTokenFactory('test-secret', 'workspace-admin');
100
+ const token = factory.mintAdmin();
101
+ const { payload } = decodeJwt(token);
102
+
103
+ assert.equal(payload.agent_name, DEFAULT_ADMIN_AGENT_NAME);
104
+ assert.equal(payload.wks, 'workspace-admin');
105
+ assert.deepEqual(payload.scopes, DEFAULT_ADMIN_SCOPES);
106
+ });
107
+
108
+ test('WorkflowTokenFactory getToken returns the token minted for an agent', () => {
109
+ const factory = new WorkflowTokenFactory('test-secret', 'workspace-123');
110
+ const token = factory.mintForAgent('builder', ['relayfile:fs:read:/src/index.ts']);
111
+
112
+ assert.equal(factory.getToken('builder'), token);
113
+ });
114
+
115
+ test('WorkflowTokenFactory uses its configured TTL when minting agent tokens', () => {
116
+ const factory = new WorkflowTokenFactory('test-secret', 'workspace-123', 45);
117
+ const token = factory.mintForAgent('builder', []);
118
+ const { payload } = decodeJwt(token);
119
+
120
+ assert.equal(payload.exp - payload.iat, 45);
121
+ });
122
+
123
+ test('mintAgentToken generates a unique JTI per token', () => {
124
+ const first = decodeJwt(
125
+ mintAgentToken({
126
+ secret: 'test-secret',
127
+ agentName: 'worker',
128
+ workspace: 'workspace-123',
129
+ scopes: [],
130
+ })
131
+ ).payload;
132
+ const second = decodeJwt(
133
+ mintAgentToken({
134
+ secret: 'test-secret',
135
+ agentName: 'worker',
136
+ workspace: 'workspace-123',
137
+ scopes: [],
138
+ })
139
+ ).payload;
140
+
141
+ assert.notEqual(first.jti, second.jti);
142
+ assert.match(first.jti, /^tok-\d+-/u);
143
+ assert.match(second.jti, /^tok-\d+-/u);
144
+ });
145
+
146
+ test('mintAgentToken includes the expected audience claims', () => {
147
+ const token = mintAgentToken({
148
+ secret: 'test-secret',
149
+ agentName: 'worker',
150
+ workspace: 'workspace-123',
151
+ scopes: [],
152
+ });
153
+
154
+ const { payload } = decodeJwt(token);
155
+
156
+ assert.deepEqual(payload.aud, ['relayauth', 'relayfile']);
157
+ });
158
+
159
+ test('mintAgentToken signs tokens with HMAC-SHA256', () => {
160
+ const secret = 'test-secret';
161
+ const token = mintAgentToken({
162
+ secret,
163
+ agentName: 'worker',
164
+ workspace: 'workspace-123',
165
+ scopes: ['relayfile:fs:read:/src/index.ts'],
166
+ });
167
+
168
+ const [header, payload, signature] = token.split('.');
169
+ const expectedSignature = createHmac('sha256', secret).update(`${header}.${payload}`).digest('base64url');
170
+
171
+ assert.equal(signature, expectedSignature);
172
+ });
@@ -0,0 +1,53 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { DEFAULT_WORKFLOW_TOKEN_TTL_SECONDS, mintAgentToken, type TokenClaims } from '../token.js';
5
+
6
+ function decodeJwtPayload(token: string): TokenClaims {
7
+ const [, payload] = token.split('.');
8
+ return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as TokenClaims;
9
+ }
10
+
11
+ test('mintAgentToken returns a valid JWT', () => {
12
+ const token = mintAgentToken({
13
+ secret: 'test-secret',
14
+ agentName: 'worker',
15
+ workspace: 'workspace-123',
16
+ scopes: ['relayfile:fs:read:/src/index.ts'],
17
+ });
18
+
19
+ const parts = token.split('.');
20
+ assert.equal(parts.length, 3);
21
+ assert.ok(parts.every((part) => /^[A-Za-z0-9_-]+$/u.test(part)));
22
+ });
23
+
24
+ test('mintAgentToken payload contains agent_name, workspace, and scopes', () => {
25
+ const scopes = ['relayfile:fs:read:/src/index.ts', 'relayfile:fs:write:/src/index.ts'];
26
+ const token = mintAgentToken({
27
+ secret: 'test-secret',
28
+ agentName: 'compiler',
29
+ workspace: 'workspace-abc',
30
+ scopes,
31
+ });
32
+
33
+ const payload = decodeJwtPayload(token);
34
+
35
+ assert.equal(payload.agent_name, 'compiler');
36
+ assert.equal(payload.wks, 'workspace-abc');
37
+ assert.equal(payload.workspace_id, 'workspace-abc');
38
+ assert.deepEqual(payload.scopes, scopes);
39
+ });
40
+
41
+ test('mintAgentToken defaults expiry to 2 hours', () => {
42
+ const token = mintAgentToken({
43
+ secret: 'test-secret',
44
+ agentName: 'worker',
45
+ workspace: 'workspace-123',
46
+ scopes: [],
47
+ });
48
+
49
+ const payload = decodeJwtPayload(token);
50
+
51
+ assert.equal(payload.exp - payload.iat, DEFAULT_WORKFLOW_TOKEN_TTL_SECONDS);
52
+ assert.equal(payload.exp - payload.iat, 2 * 60 * 60);
53
+ });
@@ -0,0 +1,104 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export interface PermissionAuditEntry {
5
+ timestamp: string;
6
+ agentName: string;
7
+ action: string;
8
+ details: Record<string, unknown>;
9
+ }
10
+
11
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
12
+
13
+ const DEFAULT_PERMISSION_AUDIT_RELATIVE_PATH = path.join('.agent-relay', 'permission-audit.json');
14
+
15
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ }
18
+
19
+ function sanitizeJsonValue(value: unknown, key?: string): JsonValue {
20
+ if (key && key.toLowerCase().includes('token')) {
21
+ return '[redacted]';
22
+ }
23
+
24
+ if (
25
+ value === null ||
26
+ typeof value === 'string' ||
27
+ typeof value === 'number' ||
28
+ typeof value === 'boolean'
29
+ ) {
30
+ return value;
31
+ }
32
+
33
+ if (Array.isArray(value)) {
34
+ return value.map((item) => sanitizeJsonValue(item));
35
+ }
36
+
37
+ if (isPlainObject(value)) {
38
+ return Object.fromEntries(
39
+ Object.entries(value).map(([entryKey, entryValue]) => [
40
+ entryKey,
41
+ sanitizeJsonValue(entryValue, entryKey),
42
+ ])
43
+ );
44
+ }
45
+
46
+ return String(value);
47
+ }
48
+
49
+ export function getDefaultPermissionAuditPath(projectDir: string): string {
50
+ return path.resolve(projectDir, DEFAULT_PERMISSION_AUDIT_RELATIVE_PATH);
51
+ }
52
+
53
+ export class PermissionAuditLog {
54
+ private readonly entries: PermissionAuditEntry[] = [];
55
+
56
+ log(entry: Omit<PermissionAuditEntry, 'timestamp'> & { timestamp?: string }): PermissionAuditEntry {
57
+ const storedEntry: PermissionAuditEntry = {
58
+ timestamp: entry.timestamp ?? new Date().toISOString(),
59
+ agentName: entry.agentName,
60
+ action: entry.action,
61
+ details: sanitizeJsonValue(entry.details) as Record<string, unknown>,
62
+ };
63
+
64
+ this.entries.push(storedEntry);
65
+ return storedEntry;
66
+ }
67
+
68
+ toJSON(): { entries: PermissionAuditEntry[] } {
69
+ return {
70
+ entries: this.entries.map((entry) => ({
71
+ timestamp: entry.timestamp,
72
+ agentName: entry.agentName,
73
+ action: entry.action,
74
+ details: { ...entry.details },
75
+ })),
76
+ };
77
+ }
78
+
79
+ async writeTo(filePath: string): Promise<void> {
80
+ await mkdir(path.dirname(filePath), { recursive: true });
81
+ await writeFile(filePath, `${JSON.stringify(this.toJSON(), null, 2)}\n`, 'utf8');
82
+ }
83
+
84
+ summary(): string {
85
+ if (this.entries.length === 0) {
86
+ return 'Permission audit: 0 entries';
87
+ }
88
+
89
+ const actionCounts = new Map<string, number>();
90
+ const agentNames = new Set<string>();
91
+
92
+ for (const entry of this.entries) {
93
+ actionCounts.set(entry.action, (actionCounts.get(entry.action) ?? 0) + 1);
94
+ agentNames.add(entry.agentName);
95
+ }
96
+
97
+ const actionSummary = [...actionCounts.entries()]
98
+ .sort(([left], [right]) => left.localeCompare(right))
99
+ .map(([action, count]) => `${action}=${count}`)
100
+ .join(', ');
101
+
102
+ return `Permission audit: ${this.entries.length} entr${this.entries.length === 1 ? 'y' : 'ies'} across ${agentNames.size} agent${agentNames.size === 1 ? '' : 's'} (${actionSummary})`;
103
+ }
104
+ }