@zq-silk/yui 2.1.0 → 2.2.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.
Files changed (102) hide show
  1. package/README.md +4 -0
  2. package/dist/cli/commandCatalog.js +24 -3
  3. package/dist/cli/commandDiscovery.js +4 -1
  4. package/dist/cli.js +23 -3078
  5. package/dist/commands/taskCommands.js +79 -21
  6. package/dist/commands/taskIntegrationCommands.js +3 -1
  7. package/dist/commands/taskUpstreamCommands.js +3 -1
  8. package/dist/context/runContextPack.js +132 -5
  9. package/dist/context/sourceRunContext.js +4 -2
  10. package/dist/context/taskContext.js +6 -1
  11. package/dist/controlPlaneCli.js +3092 -0
  12. package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
  13. package/dist/controller/jobControl.js +21 -0
  14. package/dist/executor/agentExecutor.js +1 -1
  15. package/dist/executor/effectiveLaunch.js +18 -5
  16. package/dist/executor/fileRoleLaunchPlanner.js +15 -19
  17. package/dist/integration/gitIntegrationService.js +50 -15
  18. package/dist/message/messageContinuation.js +7 -2
  19. package/dist/nativeAgent/agent.js +176 -74
  20. package/dist/nativeAgent/cliDemo.js +37 -0
  21. package/dist/nativeAgent/codingTools.js +11 -0
  22. package/dist/nativeAgent/commandTool.js +215 -0
  23. package/dist/nativeAgent/compactionDemo.js +158 -0
  24. package/dist/nativeAgent/composition.js +71 -0
  25. package/dist/nativeAgent/context/budget.js +44 -0
  26. package/dist/nativeAgent/context/index.js +339 -0
  27. package/dist/nativeAgent/context/providerCompressor.js +103 -0
  28. package/dist/nativeAgent/demo.js +12 -1
  29. package/dist/nativeAgent/evaluation/cases.js +38 -0
  30. package/dist/nativeAgent/evaluation/checks.js +91 -0
  31. package/dist/nativeAgent/evaluation/demo.js +19 -0
  32. package/dist/nativeAgent/evaluation/files.js +54 -0
  33. package/dist/nativeAgent/evaluation/fixture.js +36 -0
  34. package/dist/nativeAgent/evaluation/index.js +239 -0
  35. package/dist/nativeAgent/executionOwner.js +209 -0
  36. package/dist/nativeAgent/filePatterns.js +170 -0
  37. package/dist/nativeAgent/fileToolsSupport.js +202 -0
  38. package/dist/nativeAgent/index.js +13 -0
  39. package/dist/nativeAgent/interaction/cli.js +358 -0
  40. package/dist/nativeAgent/interaction/contracts.js +1 -0
  41. package/dist/nativeAgent/interaction/index.js +3 -0
  42. package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
  43. package/dist/nativeAgent/interaction/renderer.js +34 -0
  44. package/dist/nativeAgent/localSafety.js +258 -0
  45. package/dist/nativeAgent/model/anthropicMessages.js +204 -0
  46. package/dist/nativeAgent/model/chatCompletions.js +210 -0
  47. package/dist/nativeAgent/model/errors.js +47 -0
  48. package/dist/nativeAgent/model/gateway.js +423 -0
  49. package/dist/nativeAgent/model/index.js +7 -0
  50. package/dist/nativeAgent/model/observationAdapter.js +21 -0
  51. package/dist/nativeAgent/model/protocols.js +19 -0
  52. package/dist/nativeAgent/model/responses.js +263 -0
  53. package/dist/nativeAgent/model/types.js +1 -0
  54. package/dist/nativeAgent/model/wire.js +73 -0
  55. package/dist/nativeAgent/observability/index.js +220 -0
  56. package/dist/nativeAgent/product/catalog.js +82 -0
  57. package/dist/nativeAgent/product/config.js +295 -0
  58. package/dist/nativeAgent/product/facts.js +30 -0
  59. package/dist/nativeAgent/product/index.js +62 -0
  60. package/dist/nativeAgent/product/location.js +44 -0
  61. package/dist/nativeAgent/product/runtime.js +276 -0
  62. package/dist/nativeAgent/product/storage.js +49 -0
  63. package/dist/nativeAgent/product/tools.js +47 -0
  64. package/dist/nativeAgent/product/transport.js +54 -0
  65. package/dist/nativeAgent/projectGuidance/index.js +425 -0
  66. package/dist/nativeAgent/searchTools.js +305 -0
  67. package/dist/nativeAgent/session/backends.js +293 -0
  68. package/dist/nativeAgent/session/catalog.js +97 -0
  69. package/dist/nativeAgent/session/catalogDemo.js +87 -0
  70. package/dist/nativeAgent/session/contracts.js +1 -0
  71. package/dist/nativeAgent/session/format.js +269 -0
  72. package/dist/nativeAgent/session/index.js +5 -0
  73. package/dist/nativeAgent/session/location.js +36 -0
  74. package/dist/nativeAgent/session/sqliteFormat.js +134 -0
  75. package/dist/nativeAgent/session/store.js +248 -0
  76. package/dist/nativeAgent/textTools.js +270 -133
  77. package/dist/nativeAgent/toolManager/executor.js +290 -0
  78. package/dist/nativeAgent/toolManager/index.js +4 -0
  79. package/dist/nativeAgent/validation.js +2 -2
  80. package/dist/task/taskAuthority.js +56 -0
  81. package/dist/web/assets/client/app.js +64 -3
  82. package/dist/web/assets/client/detail.js +6 -6
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/overview.js +12 -9
  85. package/dist/web/assets/client/sidebar.js +31 -2
  86. package/dist/web/assets/shell.js +2 -1
  87. package/dist/web/assets/styles/components.js +2 -0
  88. package/dist/web/assets/styles/layout.js +11 -4
  89. package/dist/web/assets/styles/responsive.js +12 -4
  90. package/dist/web/assets/styles/views.js +16 -13
  91. package/docs/agent-result-consumption.md +16 -0
  92. package/docs/agent-result-consumption.zh-CN.md +13 -0
  93. package/docs/examples/agent-offline.mjs +194 -0
  94. package/docs/native-agent.md +283 -0
  95. package/docs/release-workflow.md +47 -9
  96. package/docs/release-workflow.zh-CN.md +36 -6
  97. package/docs/roles-and-configuration.md +32 -0
  98. package/docs/roles-and-configuration.zh-CN.md +26 -0
  99. package/package.json +1 -1
  100. package/skills/yui-leader/SKILL.md +9 -0
  101. package/skills/yui-reviewer/SKILL.md +5 -0
  102. package/skills/yui-runtime/SKILL.md +7 -0
@@ -0,0 +1,276 @@
1
+ import { createAgent, createProjectGuidance, createContextBuilder, createExecutionOwner, createLocalObserver, createModelGateway, connectModelObservations, createInteractionDiagnostics, openCli, } from '../index.js';
2
+ import { ProductError, containsCredential, createProductBinding, publicConfiguration } from './config.js';
3
+ import { createProductTransport } from './transport.js';
4
+ import { createProductTools } from './tools.js';
5
+ import { openProductStore, storageFailure } from './storage.js';
6
+ import { restoreProductLocation, createProductSession } from './location.js';
7
+ import { createRuntimeFacts } from './facts.js';
8
+ /** Product owns the streams' connection listeners, model dispatcher, store and owner.
9
+ * No hidden execution loop/catalog/policy: all execution goes through ExecutionOwner. */
10
+ export async function runProductRuntime(invocation, credential) {
11
+ let { config } = invocation;
12
+ // Rebuild on EVERY open/resume from current explicit authority, never stored history.
13
+ // Same factory for all three capabilities; do not grant the shorthand's always-allow lease.
14
+ let binding;
15
+ const observer = createLocalObserver();
16
+ let store, owner, cli;
17
+ let selected = invocation.session;
18
+ let signalExit;
19
+ let outputFailed = false;
20
+ const displayedReceipts = new Set();
21
+ const guard = (value) => {
22
+ if (credential && containsCredential(value, credential)) {
23
+ throw new ProductError('agent_execution', 'credential', 'Credential found in conversation data; preserve saved facts, do not replay.');
24
+ }
25
+ };
26
+ const write = async (value, prefix = '') => {
27
+ guard(value);
28
+ if (outputFailed)
29
+ return;
30
+ await new Promise((resolve, reject) => {
31
+ process.stdout.write(`${prefix}${JSON.stringify(value)}\n`, error => {
32
+ if (error)
33
+ reject(new ProductError('agent_output', 'stdout', 'Output disconnected; owned execution is being closed.'));
34
+ else
35
+ resolve();
36
+ });
37
+ });
38
+ };
39
+ const outputError = () => { outputFailed = true; cli?.close(); void owner?.close(); };
40
+ const terminate = () => { signalExit = 143; cli?.close(); void owner?.close(); };
41
+ const interrupt = () => {
42
+ void (async () => {
43
+ if (selected && owner) {
44
+ const page = await owner.read(selected, 0, 1);
45
+ if (page.activeTurnId !== null) {
46
+ await owner.cancel({ sessionId: selected, turnId: page.activeTurnId });
47
+ if (invocation.command === 'run')
48
+ signalExit = 130;
49
+ return;
50
+ }
51
+ }
52
+ signalExit = 130;
53
+ cli?.close();
54
+ await owner?.close();
55
+ })().catch(() => { signalExit = 130; cli?.close(); void owner?.close(); });
56
+ };
57
+ process.stdout.on('error', outputError);
58
+ process.on('SIGINT', interrupt);
59
+ process.on('SIGTERM', terminate);
60
+ // Explicit per-product dispatcher. Closing it cannot close a borrowed/global pool.
61
+ const network = createProductTransport();
62
+ try {
63
+ store = await openProductStore(config.stateDir, true);
64
+ if (selected) {
65
+ const location = await restoreProductLocation(store, selected, config);
66
+ config = { ...config, ...location };
67
+ invocation = { ...invocation, config };
68
+ }
69
+ binding = createProductBinding(invocation, credential);
70
+ const activeStore = store;
71
+ const selectedLocation = { root: config.root, cwd: config.cwd };
72
+ const admitLocation = async (id) => {
73
+ const location = await restoreProductLocation(activeStore, id, config);
74
+ guard(location);
75
+ if (location.root !== selectedLocation.root || location.cwd !== selectedLocation.cwd)
76
+ throw new ProductError('agent_config', 'location', 'This UI is bound to a different location. Close the settled process and reopen with --session ID and fresh grants.');
77
+ return location;
78
+ };
79
+ const provider = createModelGateway({
80
+ endpoint: config.endpoint, model: config.model,
81
+ account: credential ? { kind: 'bearer', token: credential } : { kind: 'none' },
82
+ stream: config.stream, retry: { maxElapsedMs: config.modelTimeoutMs },
83
+ onObservation: connectModelObservations(observer),
84
+ // Use a locally owned network pool; no global connection or account state.
85
+ transport: network.transport,
86
+ });
87
+ owner = createExecutionOwner({
88
+ store, maxSteps: config.maxSteps, observer,
89
+ ...(selected ? { sessions: [{ id: selected, title: 'Explicit selection' }] } : {}),
90
+ agent: async (recording) => {
91
+ // ExecutionOwner creates this Agent for the admitted Turn. Bind to the
92
+ // recording's identity, never the UI's mutable selected Session.
93
+ const sessionId = recording.lastReceipt.sessionId;
94
+ const location = await admitLocation(sessionId);
95
+ const currentInvocation = { ...invocation, config: { ...config, ...location } };
96
+ const currentBinding = createProductBinding(currentInvocation, credential);
97
+ const guidance = createProjectGuidance({ ...location, sessionId });
98
+ const facts = createRuntimeFacts(currentInvocation, currentBinding.binding, sessionId, credential);
99
+ return createAgent({
100
+ toolExecutor: createProductTools(currentBinding, guidance, currentInvocation, sessionId),
101
+ contextBuilder: createContextBuilder({ sources: [facts, guidance.source] }),
102
+ contextBudget: { capacity: config.contextBytes, reserveOutput: config.outputReserveBytes },
103
+ observer,
104
+ provider: { async complete(request, signal) {
105
+ guard(request);
106
+ const response = await provider.complete(request, signal);
107
+ guard(response); // Do not persist/execute a credential echoed by the model.
108
+ return response;
109
+ } },
110
+ recorder: { async record(event) { guard(event); await recording.record(event); } },
111
+ });
112
+ },
113
+ });
114
+ if (selected) {
115
+ try {
116
+ const saved = await store.load(selected);
117
+ guard(saved);
118
+ if (saved.recovery.disposition !== 'ready')
119
+ throw new Error();
120
+ }
121
+ catch {
122
+ throw new ProductError('agent_storage', 'session', 'Select an existing ready Session in this state directory; inspect unsettled/unknown facts without replay.');
123
+ }
124
+ }
125
+ else
126
+ selected = (await createProductSession(owner, store, 'Agent session', selectedLocation)).id;
127
+ if (signalExit !== undefined || outputFailed)
128
+ return signalExit ?? 1;
129
+ if (invocation.command === 'run') {
130
+ await owner.submit(selected, invocation.input);
131
+ const evidence = await owner.settle(selected);
132
+ if (!evidence?.result || evidence.failure || evidence.result.recording.status !== 'recorded') {
133
+ await write({ status: 'not_fully_saved', lastConfirmedReceipt: evidence?.receipt,
134
+ error: { code: 'agent_storage', field: 'recording',
135
+ nextAction: 'Inspect the last confirmed receipt and stored facts. No automatic retry.' } });
136
+ return 1;
137
+ }
138
+ const saved = await store.load(selected).catch(() => undefined);
139
+ const confirmed = saved && evidence.result.recording.status === 'recorded'
140
+ && evidence.receipt?.digest === saved.digest && evidence.receipt.revision === saved.revision;
141
+ // Failure evidence is not a full-turn save receipt.
142
+ if (!confirmed) {
143
+ await write({ status: 'receipt_verification_failed', lastConfirmedReceipt: evidence.receipt,
144
+ error: { code: 'agent_storage', field: 'recording',
145
+ nextAction: 'Preserve the state directory and inspect confirmed facts.' } });
146
+ return 1;
147
+ }
148
+ await write({ configuration: publicConfiguration(config), binding: binding.binding,
149
+ projectAuthority: { allowMemoryWrite: invocation.allowMemoryWrite },
150
+ result: evidence.result, receipt: evidence.receipt,
151
+ observations: observer.query().records });
152
+ return signalExit ?? (evidence.result.reason === 'completed' ? 0 : evidence.result.reason === 'cancelled'
153
+ ? 130 : evidence.result.reason === 'budget_exhausted' ? 3 : 1);
154
+ }
155
+ const activeOwner = owner;
156
+ const catalogSafe = async (operation) => {
157
+ try {
158
+ const value = await operation();
159
+ guard(value);
160
+ return value;
161
+ }
162
+ catch (error) {
163
+ throw storageFailure(error);
164
+ }
165
+ };
166
+ const catalog = {
167
+ listSessions: options => catalogSafe(() => activeStore.listSessions(options)),
168
+ getSessionInfo: id => catalogSafe(() => activeStore.getSessionInfo(id)),
169
+ readHistory: (id, options) => catalogSafe(() => activeStore.readHistory(id, options)),
170
+ async renameSession(id, title, expected) {
171
+ guard(title);
172
+ return catalogSafe(() => activeStore.renameSession(id, title, expected));
173
+ },
174
+ };
175
+ const safe = async (operation) => {
176
+ try {
177
+ const value = await operation();
178
+ guard(value);
179
+ return value;
180
+ }
181
+ catch (error) {
182
+ if (error instanceof ProductError)
183
+ throw error;
184
+ throw storageFailure(error);
185
+ }
186
+ };
187
+ const port = {
188
+ async create(title) {
189
+ guard(title);
190
+ if (!title.trim() || [...title.trim()].length > 200 || /[\p{Cc}\p{Cs}]/u.test(title))
191
+ throw new ProductError('agent_config', 'title', 'Use 1..200 Unicode characters without controls.');
192
+ const item = await createProductSession(activeOwner, activeStore, title, selectedLocation);
193
+ await catalog.renameSession(item.id, title, 0);
194
+ return item;
195
+ },
196
+ list: (offset, limit) => safe(() => activeOwner.list(offset, limit)),
197
+ async submit(id, input) {
198
+ guard(input);
199
+ await admitLocation(id);
200
+ const scope = await safe(() => activeOwner.submit(id, input));
201
+ selected = id;
202
+ return scope;
203
+ },
204
+ cancel: scope => safe(() => activeOwner.cancel(scope)),
205
+ history: (id, offset, limit) => safe(() => activeOwner.history(id, offset, limit)),
206
+ subscribe: (id, callback) => activeOwner.subscribe(id, callback),
207
+ async read(id, after, limit) {
208
+ await admitLocation(id);
209
+ const page = await safe(() => activeOwner.read(id, after, limit));
210
+ selected = id;
211
+ // Replay is a read, never a join of the current execution promise.
212
+ // Historical terminal IDs cannot select another Turn's live evidence.
213
+ for (const record of page.records) {
214
+ if (record.kind !== 'event' || record.event.data.type !== 'turn_ended')
215
+ continue;
216
+ const evidence = activeOwner.getSettledEvidence({ sessionId: id, turnId: record.event.turnId });
217
+ if (!evidence?.receipt || evidence.result?.recording.status !== 'recorded' || evidence.failure
218
+ || displayedReceipts.has(evidence.receipt.digest))
219
+ continue;
220
+ const saved = await activeStore.load(id);
221
+ if (saved.digest === evidence.receipt.digest && saved.revision === evidence.receipt.revision) {
222
+ displayedReceipts.add(evidence.receipt.digest);
223
+ await write(evidence.receipt, '[saved receipt] ');
224
+ }
225
+ }
226
+ return page;
227
+ },
228
+ };
229
+ await write({ configuration: publicConfiguration(config), binding: binding.binding,
230
+ projectAuthority: { allowMemoryWrite: invocation.allowMemoryWrite },
231
+ sessionId: selected, mode: 'local-tool-binding',
232
+ note: 'Original root/cwd are persisted atomically. Cross-location selection requires settled close and explicit reopen with fresh authority. Not an OS sandbox.' }, '[agent] ');
233
+ cli = await openCli({ sessions: port, catalog, input: process.stdin, output: process.stdout,
234
+ initialSessionId: selected, diagnostics: createInteractionDiagnostics(observer) });
235
+ if (signalExit !== undefined || outputFailed)
236
+ cli.close();
237
+ const ended = await cli.done;
238
+ // EOF/quit detaches UI only; the product owner must cancel/drain the actual
239
+ // execution before acknowledging shutdown or closing storage/network pools.
240
+ await owner.close();
241
+ const finalEvidence = await owner.settle(selected);
242
+ if (finalEvidence?.receipt && !displayedReceipts.has(finalEvidence.receipt.digest)) {
243
+ const saved = await store.load(selected).catch(() => undefined);
244
+ if (finalEvidence.result?.recording.status === 'recorded' && !finalEvidence.failure
245
+ && saved?.digest === finalEvidence.receipt.digest && saved.revision === finalEvidence.receipt.revision) {
246
+ await write(finalEvidence.receipt, '[saved receipt on close] ');
247
+ }
248
+ else
249
+ await write({ status: 'not_fully_saved', lastConfirmedReceipt: finalEvidence.receipt,
250
+ nextAction: 'Inspect saved facts; no automatic retry.' }, '[agent close] ');
251
+ }
252
+ return signalExit ?? (['display-error', 'input-error'].includes(ended.reason) || outputFailed ? 1 : 0);
253
+ }
254
+ finally {
255
+ cli?.close();
256
+ try {
257
+ await owner?.close();
258
+ }
259
+ finally {
260
+ try {
261
+ await store?.close();
262
+ }
263
+ finally {
264
+ observer.close();
265
+ try {
266
+ network.close();
267
+ }
268
+ finally {
269
+ process.stdout.off('error', outputError);
270
+ process.off('SIGINT', interrupt);
271
+ process.off('SIGTERM', terminate);
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }
@@ -0,0 +1,49 @@
1
+ import { mkdir, chmod, lstat, realpath } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { createSessionStore, createSqliteSessionBackend, SessionError, SessionMetadataSaveError } from '../index.js';
4
+ import { ProductError } from './config.js';
5
+ import { SessionSaveError } from '../session/index.js';
6
+ /** Only fixed producer codes reach diagnostics; never title/cursor/cause text. */
7
+ export function storageFailure(error) {
8
+ if (error instanceof ProductError)
9
+ return error;
10
+ const codes = ['cursor_stale', 'invalid_cursor', 'not_found', 'revision_conflict',
11
+ 'read_failed', 'unsupported_format', 'corrupt_session', 'invalid_title', 'closed', 'metadata_save_failed', 'save_failed'];
12
+ const field = error instanceof SessionError && codes.includes(error.code) ? error.code : 'catalog';
13
+ const next = field === 'cursor_stale' || field === 'invalid_cursor'
14
+ ? 'Restart from the first page with the same store, Session ID and page limit.'
15
+ : error instanceof SessionSaveError
16
+ ? `Inspect getSessionInfo and load for Session ${error.sessionId}; preserve unknown effects, never blindly create again or switch IDs.`
17
+ : error instanceof SessionMetadataSaveError
18
+ ? `Read Session ${error.sessionId}'s exact title and metadata revision before another write; do not blindly retry.`
19
+ : 'Inspect the exact selected store and Session facts; no automatic retry or replay.';
20
+ return new ProductError('agent_storage', field, next, error instanceof SessionMetadataSaveError || error instanceof SessionSaveError ? 'unknown' : undefined, error instanceof SessionMetadataSaveError || error instanceof SessionSaveError ? error.sessionId : undefined);
21
+ }
22
+ /** A browsing command opens an existing file only; execution may create it.
23
+ * Both use the real producer's supported migration chain, never another ledger. */
24
+ export async function openProductStore(directory, create) {
25
+ let store;
26
+ try {
27
+ if (create)
28
+ await mkdir(directory, { recursive: true, mode: 0o700 });
29
+ const info = await lstat(directory);
30
+ if (!info.isDirectory() || info.isSymbolicLink())
31
+ throw new Error();
32
+ const filename = join(await realpath(directory), 'sessions.sqlite');
33
+ if (!create) {
34
+ const file = await lstat(filename);
35
+ if (!file.isFile() || file.isSymbolicLink())
36
+ throw new Error();
37
+ }
38
+ store = createSessionStore(createSqliteSessionBackend(filename));
39
+ if (create)
40
+ await chmod(filename, 0o600);
41
+ return store;
42
+ }
43
+ catch (error) {
44
+ await store?.close();
45
+ if (error instanceof SessionError)
46
+ throw storageFailure(error);
47
+ throw new ProductError('agent_storage', 'stateDir', 'Select a controlled state directory with a supported Agent database; browsing never creates one.');
48
+ }
49
+ }
@@ -0,0 +1,47 @@
1
+ import { createToolExecutor } from '../index.js';
2
+ /** Compose public capabilities, not producer-private registries.
3
+ * Coding calls retain the original factory's exact live lease and permission.
4
+ * Ordinary project tools acquire no coding resource/authority. */
5
+ export function createProductTools(binding, guidance, invocation, sessionId) {
6
+ const codingNames = new Set(binding.tools.map(tool => tool.definition.name));
7
+ const tools = binding.tools.map(tool => ({
8
+ definition: tool.definition,
9
+ validate: args => tool.validate(args),
10
+ async execute(args, scope, signal, lease) {
11
+ if (lease === undefined)
12
+ return { ok: false, error: {
13
+ code: 'missing_coding_environment', message: 'Coding resource was not acquired', effect: 'none'
14
+ } };
15
+ return tool.execute(args, scope, signal, lease);
16
+ },
17
+ }));
18
+ tools.push(...guidance.tools);
19
+ return createToolExecutor({
20
+ tools: tools.filter(tool => invocation.config.tools.includes(tool.definition.name)),
21
+ environment: {
22
+ async acquire(call, signal) {
23
+ if (codingNames.has(call.call.name))
24
+ return binding.environment.acquire(call, signal);
25
+ return { value: undefined, async release() { } };
26
+ },
27
+ },
28
+ permission: {
29
+ async check(call, lease, signal) {
30
+ if (codingNames.has(call.call.name)) {
31
+ return lease === undefined ? { allowed: false, reason: 'Coding resource required' }
32
+ : binding.permission.check(call, lease, signal);
33
+ }
34
+ if (signal.aborted || lease !== undefined || call.identity.sessionId !== sessionId)
35
+ return { allowed: false, reason: 'Project Session scope does not match' };
36
+ const args = call.call.arguments;
37
+ const action = args !== null && typeof args === 'object' && !Array.isArray(args) ? args.action : undefined;
38
+ if (call.call.name === 'project_context' && ['inspect', 'load_skill', 'reference'].includes(String(action)))
39
+ return { allowed: true };
40
+ if (call.call.name === 'project_memory' && (action === 'read'
41
+ || (invocation.allowMemoryWrite && (action === 'replace' || action === 'delete'))))
42
+ return { allowed: true };
43
+ return { allowed: false, reason: 'Project memory modification requires --allow-memory-write on this invocation' };
44
+ },
45
+ },
46
+ });
47
+ }
@@ -0,0 +1,54 @@
1
+ import { Agent as HttpAgent, request as httpRequest } from 'node:http';
2
+ import { Agent as HttpsAgent, request as httpsRequest } from 'node:https';
3
+ import { Readable, addAbortSignal } from 'node:stream';
4
+ /** One product-owned pool, no global fetch dispatcher. No redirects or retries.
5
+ * Gateway owns each Response reader; cancelling it destroys its Node response.
6
+ * Closing the pool after execution has settled releases idle owned sockets. */
7
+ export function createProductTransport() {
8
+ const http = new HttpAgent({ keepAlive: true, maxSockets: 1 });
9
+ const https = new HttpsAgent({ keepAlive: true, maxSockets: 1 });
10
+ let closed = false;
11
+ return {
12
+ transport(endpoint, init) {
13
+ return new Promise((resolve, reject) => {
14
+ if (closed) {
15
+ reject(new Error('Product transport closed'));
16
+ return;
17
+ }
18
+ const secure = new URL(endpoint).protocol === 'https:';
19
+ const request = (secure ? httpsRequest : httpRequest)(endpoint, {
20
+ method: init.method, headers: init.headers, signal: init.signal, agent: secure ? https : http,
21
+ }, response => {
22
+ addAbortSignal(init.signal, response);
23
+ try {
24
+ const headers = new Headers();
25
+ for (const [key, value] of Object.entries(response.headers)) {
26
+ if (value !== undefined)
27
+ headers.set(key, Array.isArray(value) ? value.join(', ') : value);
28
+ }
29
+ const status = response.statusCode ?? 500;
30
+ // Response forbids bodies on these statuses. Release the actual body
31
+ // regardless; Gateway will classify the empty response truthfully.
32
+ if ([204, 205, 304].includes(status)) {
33
+ response.destroy();
34
+ resolve(new Response(null, { status, headers }));
35
+ }
36
+ else
37
+ resolve(new Response(Readable.toWeb(response), { status, headers }));
38
+ }
39
+ catch (error) {
40
+ response.destroy();
41
+ reject(error);
42
+ }
43
+ });
44
+ request.on('error', reject);
45
+ request.end(init.body);
46
+ });
47
+ },
48
+ close() { if (!closed) {
49
+ closed = true;
50
+ http.destroy();
51
+ https.destroy();
52
+ } },
53
+ };
54
+ }