@corenel/harness 0.23.0 → 0.24.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.
- package/dist/approvals/service.d.ts +3 -0
- package/dist/approvals/service.d.ts.map +1 -1
- package/dist/approvals/service.js +13 -0
- package/dist/approvals/service.js.map +1 -1
- package/dist/approvals/suggest.js +1 -1
- package/dist/approvals/suggest.js.map +1 -1
- package/dist/chat/engine.d.ts.map +1 -1
- package/dist/chat/engine.js +5 -3
- package/dist/chat/engine.js.map +1 -1
- package/dist/chat/types.d.ts +1 -0
- package/dist/chat/types.d.ts.map +1 -1
- package/dist/core/loop.d.ts.map +1 -1
- package/dist/core/loop.js +5 -4
- package/dist/core/loop.js.map +1 -1
- package/dist/core/spawn.d.ts +3 -1
- package/dist/core/spawn.d.ts.map +1 -1
- package/dist/core/spawn.js +16 -1
- package/dist/core/spawn.js.map +1 -1
- package/dist/dag/executeNode.js +1 -1
- package/dist/dag/executeNode.js.map +1 -1
- package/dist/guardrail/builtins.d.ts.map +1 -1
- package/dist/guardrail/builtins.js +2 -0
- package/dist/guardrail/builtins.js.map +1 -1
- package/dist/guardrail/permission-service.d.ts +27 -0
- package/dist/guardrail/permission-service.d.ts.map +1 -1
- package/dist/guardrail/permission-service.js +47 -5
- package/dist/guardrail/permission-service.js.map +1 -1
- package/dist/prompts/library/defaults.generated.d.ts.map +1 -1
- package/dist/prompts/library/defaults.generated.js +15 -14
- package/dist/prompts/library/defaults.generated.js.map +1 -1
- package/dist/sessions/fileSessionStore.d.ts.map +1 -1
- package/dist/sessions/fileSessionStore.js +18 -4
- package/dist/sessions/fileSessionStore.js.map +1 -1
- package/dist/sessions/jsonl.d.ts +1 -0
- package/dist/sessions/jsonl.d.ts.map +1 -1
- package/dist/sessions/jsonl.js +10 -0
- package/dist/sessions/jsonl.js.map +1 -1
- package/dist/sidecar/accessTestRig.d.ts +9 -2
- package/dist/sidecar/accessTestRig.d.ts.map +1 -1
- package/dist/sidecar/accessTestRig.js +13 -6
- package/dist/sidecar/accessTestRig.js.map +1 -1
- package/dist/sidecar/host.d.ts +47 -0
- package/dist/sidecar/host.d.ts.map +1 -1
- package/dist/sidecar/host.js +135 -1
- package/dist/sidecar/host.js.map +1 -1
- package/dist/sidecar/memberGate.d.ts.map +1 -1
- package/dist/sidecar/memberGate.js +8 -0
- package/dist/sidecar/memberGate.js.map +1 -1
- package/dist/sidecar/server.d.ts +88 -0
- package/dist/sidecar/server.d.ts.map +1 -1
- package/dist/sidecar/server.js +448 -49
- package/dist/sidecar/server.js.map +1 -1
- package/dist/templateGuard.d.ts +21 -0
- package/dist/templateGuard.d.ts.map +1 -0
- package/dist/templateGuard.js +150 -0
- package/dist/templateGuard.js.map +1 -0
- package/dist/tools/builtins.d.ts +1 -0
- package/dist/tools/builtins.d.ts.map +1 -1
- package/dist/tools/builtins.js +26 -3
- package/dist/tools/builtins.js.map +1 -1
- package/dist/tools/namespaces.d.ts +1 -1
- package/dist/tools/namespaces.d.ts.map +1 -1
- package/dist/tools/namespaces.js +6 -2
- package/dist/tools/namespaces.js.map +1 -1
- package/dist/tools/skillTool.d.ts +29 -0
- package/dist/tools/skillTool.d.ts.map +1 -1
- package/dist/tools/skillTool.js +73 -10
- package/dist/tools/skillTool.js.map +1 -1
- package/package.json +2 -2
package/dist/sidecar/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CHAT_FEATURE, CHAT_ID_RE, decodePcm16, FLEET_LOGINS_DENIED, SIGN_IN_PAUSED_DENIED, SPEECH_FEATURE, AGENT_CRED_ERROR_CODES, agentCredHeadersProblem, DEFAULT_STATE_DIR, PEER_DAEMON_FEATURE, TEAM_MEMBERS_FEATURE, RUN_TASK_FEATURE, RUN_PLAN_FEATURE, CREW_DEPLOY_FEATURE, FLEET_ITEMS_FEATURE, FLEET_USE_FEATURE, FLEET_USE_LIMITS, isWorkflowCallInputs, DEVICES_FEATURE, AGENT_ACCESS_FEATURE, AGENT_RUNS_DEFAULT_LIMIT, AGENT_RUNS_FEATURE, AGENT_RUNS_MAX_LIMIT, PEER_LINKS_FEATURE, CHILD_EVENTS_FEATURE, FS_CHANGED_FEATURE, ATTENDANCE_LEAVE_FEATURE, MCP_ALLOW_FEATURE, MCP_ADMIN_ERROR_CODES, CREW_DEFINITION_FILES, crewConfigTarget, denialNote, isRunOrigin, cleanPerson } from '@corenel/protocol';
|
|
1
|
+
import { CHAT_FEATURE, CHAT_ID_RE, decodePcm16, FLEET_LOGINS_DENIED, SIGN_IN_PAUSED_DENIED, SPEECH_FEATURE, AGENT_CRED_ERROR_CODES, agentCredHeadersProblem, DEFAULT_STATE_DIR, PEER_DAEMON_FEATURE, TEAM_MEMBERS_FEATURE, RUN_TASK_FEATURE, RUN_PLAN_FEATURE, CREW_DEPLOY_FEATURE, FLEET_ITEMS_FEATURE, FLEET_USE_FEATURE, KNOWLEDGE_FEATURE, KNOWLEDGE_OFF_FEATURE, KNOWLEDGE_NOT_KEPT, KNOWLEDGE_WRITE_ADMIN_ONLY, KNOWLEDGE_ADMIN_ONLY, KNOWLEDGE_UNREACHABLE, knowledgeConflictSentence, SIDECAR_KNOWLEDGE_TOOLS, SIDECAR_KNOWLEDGE_WRITE, FLEET_USE_LIMITS, isWorkflowCallInputs, DEVICES_FEATURE, AGENT_ACCESS_FEATURE, AGENT_RUNS_DEFAULT_LIMIT, AGENT_RUNS_FEATURE, AGENT_RUNS_MAX_LIMIT, PEER_LINKS_FEATURE, CHILD_EVENTS_FEATURE, FS_CHANGED_FEATURE, ATTENDANCE_LEAVE_FEATURE, ATTENDANCE_JOIN_FEATURE, MCP_ALLOW_FEATURE, MCP_ADMIN_ERROR_CODES, CREW_DEFINITION_FILES, crewConfigTarget, denialNote, isRunOrigin, cleanPerson } from '@corenel/protocol';
|
|
2
2
|
import { SIDECAR_PROTOCOL, errMessage, isLoopbackHost, isLoopbackAddress, parseMcpToolAddress, isWebPushSubscription, MAX_ASSERTION_LEN, isClerkOrgId, MAX_WORKFLOW_PACKAGE_B64, MAX_WORKFLOW_REPLY_ERROR, WORKFLOW_RUNNER_FEATURE, } from '@corenel/protocol';
|
|
3
3
|
import { activityForViewer, askForViewer, withoutViewerClaims } from './askView.js';
|
|
4
4
|
import { capabilityMissingMessage } from './hostCapability.js';
|
|
@@ -12,6 +12,50 @@ import { fsChangesFor } from './fsChanges.js';
|
|
|
12
12
|
function clipReplyError(error) {
|
|
13
13
|
return error.length > MAX_WORKFLOW_REPLY_ERROR ? error.slice(0, MAX_WORKFLOW_REPLY_ERROR) : error;
|
|
14
14
|
}
|
|
15
|
+
/** A folder as a caller who does not manage the folders sees it: no path, and
|
|
16
|
+
* an error that is a fixed sentence naming the folder by its label only. */
|
|
17
|
+
function knowledgeViewForMember(f) {
|
|
18
|
+
const view = { id: f.id, label: f.label, writable: f.writable, state: f.state, origin: f.origin };
|
|
19
|
+
if (f.state === 'conflict') {
|
|
20
|
+
if (f.conflict)
|
|
21
|
+
view.conflict = f.conflict;
|
|
22
|
+
view.error = knowledgeConflictSentence(f.label, f.conflict);
|
|
23
|
+
}
|
|
24
|
+
else if (f.state === 'unreachable') {
|
|
25
|
+
view.error = KNOWLEDGE_UNREACHABLE;
|
|
26
|
+
}
|
|
27
|
+
return view;
|
|
28
|
+
}
|
|
29
|
+
/** Only the wire fields of a folder, so nothing else a port holds is sent. */
|
|
30
|
+
function knowledgeViewForManager(f) {
|
|
31
|
+
return {
|
|
32
|
+
id: f.id, label: f.label, writable: f.writable, state: f.state, origin: f.origin,
|
|
33
|
+
...(f.conflict ? { conflict: f.conflict } : {}),
|
|
34
|
+
...(f.error !== undefined ? { error: f.error } : {}),
|
|
35
|
+
...(f.path !== undefined ? { path: f.path } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** A `knowledge-set` `add` as sent, checked field by field; null when its shape is wrong. */
|
|
39
|
+
function knowledgeAddOf(v) {
|
|
40
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
41
|
+
return null;
|
|
42
|
+
const o = v;
|
|
43
|
+
if (typeof o.path !== 'string')
|
|
44
|
+
return null;
|
|
45
|
+
if (o.writable !== undefined && typeof o.writable !== 'boolean')
|
|
46
|
+
return null;
|
|
47
|
+
if (o.label !== undefined && typeof o.label !== 'string')
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
path: o.path,
|
|
51
|
+
...(o.writable === true ? { writable: true } : {}),
|
|
52
|
+
...(typeof o.label === 'string' ? { label: o.label } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** What a knowledge-set with neither or both of add and remove, or a malformed one, answers. */
|
|
56
|
+
export const KNOWLEDGE_SET_SHAPE = 'Give one folder to add or the id of one folder to remove.';
|
|
57
|
+
/** What a knowledge message answers a caller who does not manage the folders when the port throws. */
|
|
58
|
+
export const KNOWLEDGE_FAILED = 'The sidecar could not answer that knowledge request. Its admin can find the reason in its log.';
|
|
15
59
|
/** The refusal for workflow inputs that fail isWorkflowCallInputs. */
|
|
16
60
|
export const FLEET_WORKFLOW_INPUTS_ERROR = `workflow inputs must be an object of at most ${FLEET_USE_LIMITS.inputsBytes / 1024} KiB`;
|
|
17
61
|
/** At most this many fleet workflow calls open at once on one connection (each loads and verifies its archive). */
|
|
@@ -80,6 +124,11 @@ export const AGENT_CRED_FAILED = 'Agent credentials could not be checked or chan
|
|
|
80
124
|
export const AGENT_CRED_LOGIN_FAILED = 'The sign-in did not finish. The daemon log has the reason.';
|
|
81
125
|
export const AGENT_CRED_SOLO = 'This daemon does not keep agent access.';
|
|
82
126
|
export const MCP_ADMIN_ONLY = "In team mode only the daemon admin can use or change this machine's MCP servers directly.";
|
|
127
|
+
/** A command runs as the daemon's OS user, outside every root and every
|
|
128
|
+
* per-member check, so on a team daemon it is the admin's alone. */
|
|
129
|
+
export const COMMANDS_ADMIN_ONLY = 'In team mode only the daemon admin can run commands on this machine directly.';
|
|
130
|
+
/** The tools a connected client could use to run a command or reach a running one. */
|
|
131
|
+
const COMMAND_TOOLS = new Set(['sidecar_shell_exec', 'proc_start', 'proc_stop', 'proc_list']);
|
|
83
132
|
export const SESSION_REFUSED = 'In team mode a session is reachable only by the member who created it, or the daemon admin.';
|
|
84
133
|
export const SNAPSHOT_REFUSED = 'In team mode you can restore only from a snapshot of a session you created.';
|
|
85
134
|
export const SNAPSHOT_NAME_REFUSED = 'In team mode a snapshot name is chosen by the daemon; only the daemon admin can name one.';
|
|
@@ -814,6 +863,15 @@ export class SidecarServer {
|
|
|
814
863
|
writable: true,
|
|
815
864
|
value: false
|
|
816
865
|
});
|
|
866
|
+
/** The welcome was sent: the hello is finished. An `attendance-join` is
|
|
867
|
+
* read only after it, so it cannot attach in the middle of the hello (whose
|
|
868
|
+
* own attach, or a leave that arrived meanwhile, decides that). */
|
|
869
|
+
Object.defineProperty(this, "welcomed", {
|
|
870
|
+
enumerable: true,
|
|
871
|
+
configurable: true,
|
|
872
|
+
writable: true,
|
|
873
|
+
value: false
|
|
874
|
+
});
|
|
817
875
|
/** This connection's seat for chat ask_user (verified connections only),
|
|
818
876
|
* and its leave function, called once on close. */
|
|
819
877
|
Object.defineProperty(this, "askSeat", {
|
|
@@ -1366,6 +1424,10 @@ export class SidecarServer {
|
|
|
1366
1424
|
void this.onAcpStart(m);
|
|
1367
1425
|
return;
|
|
1368
1426
|
case 'acp-set-mode':
|
|
1427
|
+
if (!this.authed) {
|
|
1428
|
+
this.ch.send({ t: 'acp-error', id: m.id, error: 'not authenticated' });
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1369
1431
|
void this.onAcpSetMode(m);
|
|
1370
1432
|
return;
|
|
1371
1433
|
case 'acp-prompt':
|
|
@@ -1405,6 +1467,26 @@ export class SidecarServer {
|
|
|
1405
1467
|
this.leftAttendance = true;
|
|
1406
1468
|
this.attendedDetach?.();
|
|
1407
1469
|
this.attendedDetach = undefined;
|
|
1470
|
+
/* Its ask_user seat goes too: a question shown on a connection that
|
|
1471
|
+
* is not attending would wait on nobody. The seat object stays, for
|
|
1472
|
+
* a join, and an ask still open is shown to it again then. */
|
|
1473
|
+
this.askSeatLeave?.();
|
|
1474
|
+
this.askSeatLeave = undefined;
|
|
1475
|
+
return;
|
|
1476
|
+
case 'attendance-join':
|
|
1477
|
+
/* The client takes this connection back for the person (a browser
|
|
1478
|
+
* that switched back to this machine): it attends again, and the
|
|
1479
|
+
* attach replays what is waiting, as at hello. Only after a leave on
|
|
1480
|
+
* THIS connection, so an attendance is never held twice, and only
|
|
1481
|
+
* once the hello is finished (welcomed). A peer daemon never gets
|
|
1482
|
+
* here (its messages are filtered above) and is never attached
|
|
1483
|
+
* (attachAttendance). No reply. */
|
|
1484
|
+
if (!this.authed || !this.welcomed || this.closed || !this.leftAttendance)
|
|
1485
|
+
return;
|
|
1486
|
+
this.leftAttendance = false;
|
|
1487
|
+
this.attachAttendance();
|
|
1488
|
+
if (this.askSeat && !this.askSeatLeave)
|
|
1489
|
+
this.askSeatLeave = this.askUsers().join(this.askSeat);
|
|
1408
1490
|
return;
|
|
1409
1491
|
case 'roots-browse':
|
|
1410
1492
|
// Auth-gated like every other request: directory enumeration must never
|
|
@@ -1435,6 +1517,10 @@ export class SidecarServer {
|
|
|
1435
1517
|
this.ch.send({ t: 'proc-error', id: m.id, error: 'not authenticated' });
|
|
1436
1518
|
return;
|
|
1437
1519
|
}
|
|
1520
|
+
if (this.commandRefusal()) {
|
|
1521
|
+
this.ch.send({ t: 'proc-error', id: m.id, error: COMMANDS_ADMIN_ONLY });
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1438
1524
|
void this.onProcSpawn(m);
|
|
1439
1525
|
return;
|
|
1440
1526
|
case 'proc-attach':
|
|
@@ -1442,6 +1528,10 @@ export class SidecarServer {
|
|
|
1442
1528
|
this.ch.send({ t: 'proc-attach-result', id: m.id, ok: false, error: 'not authenticated' });
|
|
1443
1529
|
return;
|
|
1444
1530
|
}
|
|
1531
|
+
if (this.commandRefusal()) {
|
|
1532
|
+
this.ch.send({ t: 'proc-attach-result', id: m.id, ok: false, error: COMMANDS_ADMIN_ONLY });
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1445
1535
|
this.onProcAttach(m);
|
|
1446
1536
|
return;
|
|
1447
1537
|
case 'proc-input':
|
|
@@ -1449,6 +1539,10 @@ export class SidecarServer {
|
|
|
1449
1539
|
this.ch.send({ t: 'proc-input-result', id: m.id, ok: false, error: 'not authenticated' });
|
|
1450
1540
|
return;
|
|
1451
1541
|
}
|
|
1542
|
+
if (this.commandRefusal()) {
|
|
1543
|
+
this.ch.send({ t: 'proc-input-result', id: m.id, ok: false, error: COMMANDS_ADMIN_ONLY });
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1452
1546
|
this.onProcSimple(m.id, 'proc-input-result', () => this.requireProcs().write(m.procId, m.text));
|
|
1453
1547
|
return;
|
|
1454
1548
|
case 'proc-kill':
|
|
@@ -1456,6 +1550,10 @@ export class SidecarServer {
|
|
|
1456
1550
|
this.ch.send({ t: 'proc-kill-result', id: m.id, ok: false, error: 'not authenticated' });
|
|
1457
1551
|
return;
|
|
1458
1552
|
}
|
|
1553
|
+
if (this.commandRefusal()) {
|
|
1554
|
+
this.ch.send({ t: 'proc-kill-result', id: m.id, ok: false, error: COMMANDS_ADMIN_ONLY });
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1459
1557
|
this.onProcSimple(m.id, 'proc-kill-result', () => this.requireProcs().kill(m.procId));
|
|
1460
1558
|
return;
|
|
1461
1559
|
case 'stt-start': {
|
|
@@ -1524,7 +1622,8 @@ export class SidecarServer {
|
|
|
1524
1622
|
// An empty list is the honest answer for a daemon without the capability:
|
|
1525
1623
|
// it is running nothing, and an error here would make the common "what is
|
|
1526
1624
|
// running?" probe noisy on every sidecar that never allowed shell.
|
|
1527
|
-
|
|
1625
|
+
// A team member is shown none: the table holds other people's commands.
|
|
1626
|
+
if (!this.authed || this.commandRefusal()) {
|
|
1528
1627
|
this.ch.send({ t: 'proc-list-result', id: m.id, procs: [] });
|
|
1529
1628
|
return;
|
|
1530
1629
|
}
|
|
@@ -1631,6 +1730,41 @@ export class SidecarServer {
|
|
|
1631
1730
|
}
|
|
1632
1731
|
void this.onFleetItems(m);
|
|
1633
1732
|
return;
|
|
1733
|
+
case 'knowledge-list':
|
|
1734
|
+
if (!this.authed) {
|
|
1735
|
+
this.ch.send({ t: 'knowledge-list-result', id: m.id, reply: { ok: false, error: 'not authenticated' } });
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
this.onKnowledgeList(m);
|
|
1739
|
+
return;
|
|
1740
|
+
case 'knowledge-set':
|
|
1741
|
+
if (!this.authed) {
|
|
1742
|
+
this.ch.send({ t: 'knowledge-set-result', id: m.id, reply: { ok: false, error: 'not authenticated' } });
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
void this.onKnowledgeSet(m);
|
|
1746
|
+
return;
|
|
1747
|
+
case 'knowledge-files':
|
|
1748
|
+
if (!this.authed) {
|
|
1749
|
+
this.ch.send({ t: 'knowledge-files-result', id: m.id, reply: { ok: false, error: 'not authenticated' } });
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
void this.onKnowledgeFiles(m);
|
|
1753
|
+
return;
|
|
1754
|
+
case 'knowledge-file':
|
|
1755
|
+
if (!this.authed) {
|
|
1756
|
+
this.ch.send({ t: 'knowledge-file-result', id: m.id, reply: { ok: false, error: 'not authenticated' } });
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
void this.onKnowledgeFile(m);
|
|
1760
|
+
return;
|
|
1761
|
+
case 'knowledge-changes':
|
|
1762
|
+
if (!this.authed) {
|
|
1763
|
+
this.ch.send({ t: 'knowledge-changes-result', id: m.id, reply: { ok: false, error: 'not authenticated' } });
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
void this.onKnowledgeChanges(m);
|
|
1767
|
+
return;
|
|
1634
1768
|
case 'fleet-skill-catalog':
|
|
1635
1769
|
if (!this.authed) {
|
|
1636
1770
|
this.ch.send({ t: 'fleet-skill-catalog-result', id: m.id, reply: { ok: false, error: 'not authenticated' } });
|
|
@@ -2583,49 +2717,13 @@ export class SidecarServer {
|
|
|
2583
2717
|
this.ch.send({ t: 'ask-user-resolved', id, by }); },
|
|
2584
2718
|
};
|
|
2585
2719
|
this.askSeat = seat;
|
|
2586
|
-
|
|
2720
|
+
// Seated only while attending: a leave during the hello holds here too.
|
|
2721
|
+
if (!this.leftAttendance)
|
|
2722
|
+
this.askSeatLeave = this.askUsers().join(seat);
|
|
2587
2723
|
}
|
|
2588
2724
|
// A leave that arrived while this hello was still being checked holds.
|
|
2589
|
-
if (
|
|
2590
|
-
|
|
2591
|
-
* send below checks the status at the moment it is made, so a member
|
|
2592
|
-
* approved later starts receiving without reconnecting. */
|
|
2593
|
-
const attended = {
|
|
2594
|
-
// Shaped for THIS connection (E5.2): the requester's own copy names nobody.
|
|
2595
|
-
send: (ask) => { if (!this.memberPending())
|
|
2596
|
-
this.ch.send({ t: 'permission-ask', ...askForViewer(ask, this.verifiedUserId) }); },
|
|
2597
|
-
resolved: (id, decision, by, byClientId) => {
|
|
2598
|
-
if (!this.memberPending())
|
|
2599
|
-
this.ch.send({ t: 'permission-resolved', id, decision, by, ...(byClientId ? { byClientId } : {}) });
|
|
2600
|
-
},
|
|
2601
|
-
// Team mode: filtered by role (Task 16b). The registry calls this for
|
|
2602
|
-
// the live broadcast AND for the replay at attach, so both are covered.
|
|
2603
|
-
// An active member still gets only what their role allows.
|
|
2604
|
-
runActivity: (activity) => { if (!this.memberPending())
|
|
2605
|
-
this.sendRunActivity(activity); },
|
|
2606
|
-
registrations: (registrations) => { if (!this.memberPending())
|
|
2607
|
-
this.ch.send({ t: 'registrations', registrations }); },
|
|
2608
|
-
approvalSuggestion: (s) => { if (!this.memberPending())
|
|
2609
|
-
this.ch.send({ t: 'approvals-suggest', ...s }); },
|
|
2610
|
-
};
|
|
2611
|
-
// Retained so `permission-answer` can name THIS connection as the
|
|
2612
|
-
// answerer -- the registry identifies an answerer by the object it
|
|
2613
|
-
// attached, not by anything the answer message claims.
|
|
2614
|
-
this.attended = attended;
|
|
2615
|
-
/* The identity is what routes an ask under `--mode team`. `clientId` is
|
|
2616
|
-
* the key this connection was FILED under (mapped through `clientKey`
|
|
2617
|
-
* where the daemon supplies one), i.e. the same value its RunIdentity
|
|
2618
|
-
* carries -- which is what makes a run's owner and its ask's recipient
|
|
2619
|
-
* the same connection. `accounts` are CLAIMED, carried for display, and
|
|
2620
|
-
* widen nothing; only `verifiedUserId` (proven at pairing) widens a
|
|
2621
|
-
* delivery to the same user's other devices. See the registry's
|
|
2622
|
-
* `recipients`. */
|
|
2623
|
-
this.attendedDetach = this.opts.attendance.attach(attended, {
|
|
2624
|
-
clientId: this.clientId ?? '',
|
|
2625
|
-
accounts,
|
|
2626
|
-
...(this.verifiedUserId ? { verifiedUserId: this.verifiedUserId } : {}),
|
|
2627
|
-
});
|
|
2628
|
-
}
|
|
2725
|
+
if (!isPeerDaemon && !this.leftAttendance)
|
|
2726
|
+
this.attachAttendance();
|
|
2629
2727
|
/* The registry replays in-flight runs at attach, and the client relies on
|
|
2630
2728
|
* that replay landing BEFORE `welcome` (it prunes the runs it still holds
|
|
2631
2729
|
* as stale once welcomed; see pruneStaleDaemonRuns). Team mode filters the
|
|
@@ -2656,6 +2754,53 @@ export class SidecarServer {
|
|
|
2656
2754
|
}
|
|
2657
2755
|
this.sendWelcome(dirName);
|
|
2658
2756
|
}
|
|
2757
|
+
/** Attaches THIS connection to the attendance registry, which replays to it
|
|
2758
|
+
* every outstanding ask it may see, the registrations, the live runs and the
|
|
2759
|
+
* task runs that settled recently. Called from `hello`, and again by an
|
|
2760
|
+
* `attendance-join` after an `attendance-leave`. Never twice at once: an
|
|
2761
|
+
* attachment already held is kept as it is. */
|
|
2762
|
+
attachAttendance() {
|
|
2763
|
+
if (!this.opts.attendance || this.peerDaemon || this.attendedDetach)
|
|
2764
|
+
return;
|
|
2765
|
+
/* A pending member (Part E) is never a recipient of anything: every
|
|
2766
|
+
* send below checks the status at the moment it is made, so a member
|
|
2767
|
+
* approved later starts receiving without reconnecting. */
|
|
2768
|
+
const attended = {
|
|
2769
|
+
// Shaped for THIS connection (E5.2): the requester's own copy names nobody.
|
|
2770
|
+
send: (ask) => { if (!this.memberPending())
|
|
2771
|
+
this.ch.send({ t: 'permission-ask', ...askForViewer(ask, this.verifiedUserId) }); },
|
|
2772
|
+
resolved: (id, decision, by, byClientId) => {
|
|
2773
|
+
if (!this.memberPending())
|
|
2774
|
+
this.ch.send({ t: 'permission-resolved', id, decision, by, ...(byClientId ? { byClientId } : {}) });
|
|
2775
|
+
},
|
|
2776
|
+
// Team mode: filtered by role (Task 16b). The registry calls this for
|
|
2777
|
+
// the live broadcast AND for the replay at attach, so both are covered.
|
|
2778
|
+
// An active member still gets only what their role allows.
|
|
2779
|
+
runActivity: (activity) => { if (!this.memberPending())
|
|
2780
|
+
this.sendRunActivity(activity); },
|
|
2781
|
+
registrations: (registrations) => { if (!this.memberPending())
|
|
2782
|
+
this.ch.send({ t: 'registrations', registrations }); },
|
|
2783
|
+
approvalSuggestion: (s) => { if (!this.memberPending())
|
|
2784
|
+
this.ch.send({ t: 'approvals-suggest', ...s }); },
|
|
2785
|
+
};
|
|
2786
|
+
// Retained so `permission-answer` can name THIS connection as the
|
|
2787
|
+
// answerer -- the registry identifies an answerer by the object it
|
|
2788
|
+
// attached, not by anything the answer message claims.
|
|
2789
|
+
this.attended = attended;
|
|
2790
|
+
/* The identity is what routes an ask under `--mode team`. `clientId` is
|
|
2791
|
+
* the key this connection was FILED under (mapped through `clientKey`
|
|
2792
|
+
* where the daemon supplies one), i.e. the same value its RunIdentity
|
|
2793
|
+
* carries -- which is what makes a run's owner and its ask's recipient
|
|
2794
|
+
* the same connection. `accounts` are CLAIMED, carried for display, and
|
|
2795
|
+
* widen nothing; only `verifiedUserId` (proven at pairing) widens a
|
|
2796
|
+
* delivery to the same user's other devices. See the registry's
|
|
2797
|
+
* `recipients`. */
|
|
2798
|
+
this.attendedDetach = this.opts.attendance.attach(attended, {
|
|
2799
|
+
clientId: this.clientId ?? '',
|
|
2800
|
+
accounts: this.accounts,
|
|
2801
|
+
...(this.verifiedUserId ? { verifiedUserId: this.verifiedUserId } : {}),
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2659
2804
|
/** Waits for this connection's `identity-assert`, with NO timeout of its
|
|
2660
2805
|
* own -- the overall bound is `awaitAndVerifyIdentity`'s, covering this wait
|
|
2661
2806
|
* AND the verify call together, so the whole leg is bounded at
|
|
@@ -2766,6 +2911,7 @@ export class SidecarServer {
|
|
|
2766
2911
|
}
|
|
2767
2912
|
}
|
|
2768
2913
|
sendWelcome(dirName) {
|
|
2914
|
+
this.welcomed = true;
|
|
2769
2915
|
/* Remembered BEFORE the welcome goes out, because `self` and the
|
|
2770
2916
|
* forget-your-own-tree refusal are both answered from it. A daemon too old
|
|
2771
2917
|
* to resolve one, or a client with no identity, leaves it undefined -- and
|
|
@@ -2832,7 +2978,7 @@ export class SidecarServer {
|
|
|
2832
2978
|
// Whether a write another client makes reaches this one as fs-changed.
|
|
2833
2979
|
...(this.opts.fsChanges ? [FS_CHANGED_FEATURE] : []),
|
|
2834
2980
|
// A connection may stop attending while it stays open (attendance-leave).
|
|
2835
|
-
...(this.opts.attendance ? [ATTENDANCE_LEAVE_FEATURE] : []),
|
|
2981
|
+
...(this.opts.attendance ? [ATTENDANCE_LEAVE_FEATURE, ATTENDANCE_JOIN_FEATURE] : []),
|
|
2836
2982
|
...(this.opts.acp ? ['acp'] : []),
|
|
2837
2983
|
...(this.opts.roots?.areas.length ? ['add-roots'] : []),
|
|
2838
2984
|
...(this.opts.roots?.areas.length && this.opts.roots.create ? ['create-roots'] : []),
|
|
@@ -2865,6 +3011,11 @@ export class SidecarServer {
|
|
|
2865
3011
|
// Whether this sidecar lists its fleet items. Only with a real port.
|
|
2866
3012
|
...(this.opts.fleetItems ? [FLEET_ITEMS_FEATURE] : []),
|
|
2867
3013
|
...(this.opts.fleetUse ? [FLEET_USE_FEATURE] : []),
|
|
3014
|
+
// Whether this sidecar answers the knowledge tools and the knowledge-*
|
|
3015
|
+
// messages over its own folders. Only with a real port, which cli.ts sets.
|
|
3016
|
+
// While the fleet has turned knowledge off (knowledgeOffNow) it says
|
|
3017
|
+
// KNOWLEDGE_OFF_FEATURE instead, so a client does not read it as an old sidecar.
|
|
3018
|
+
...(this.opts.knowledge ? [this.knowledgeOffNow(this.opts.knowledge) === null ? KNOWLEDGE_FEATURE : KNOWLEDGE_OFF_FEATURE] : []),
|
|
2868
3019
|
// Whether this daemon runs deployed workflows. Only with a real port, so
|
|
2869
3020
|
// a client offers the Sidecar deploy target only where it would run.
|
|
2870
3021
|
...(this.opts.workflows ? [WORKFLOW_RUNNER_FEATURE] : []),
|
|
@@ -3228,6 +3379,41 @@ export class SidecarServer {
|
|
|
3228
3379
|
this.ch.send({ t: 'invoke-result', id: m.id, ok: false, error: `unknown tool ${m.name}` });
|
|
3229
3380
|
return;
|
|
3230
3381
|
}
|
|
3382
|
+
/* TEAM MODE: COMMANDS ARE THE ADMIN'S (security review 2026-11-12). The
|
|
3383
|
+
* agent-access gate below decides writes into agent folders only; a shell
|
|
3384
|
+
* command is not a path, so it never saw one, and any admitted member could
|
|
3385
|
+
* run one as the daemon's OS user and read the state dir with it. */
|
|
3386
|
+
if (COMMAND_TOOLS.has(m.name)) {
|
|
3387
|
+
const refusal = this.commandRefusal();
|
|
3388
|
+
if (refusal) {
|
|
3389
|
+
this.opts.log?.(`invoke ${m.name} refused: ${refusal}`);
|
|
3390
|
+
this.ch.send({ t: 'invoke-result', id: m.id, ok: false, error: refusal });
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
/* TEAM MODE: THIS SIDECAR'S KNOWLEDGE IS THE ADMIN'S TO CHANGE (host
|
|
3395
|
+
* knowledge spec, section 3). Refused here, whatever the browser offered,
|
|
3396
|
+
* and shown to the model as the tool result, as the tool's own refusals are. */
|
|
3397
|
+
/* KNOWLEDGE THE FLEET TURNED OFF is refused on every connection, one
|
|
3398
|
+
* opened before the change included (its welcome still lists the feature),
|
|
3399
|
+
* and shown to the model as the tool result. */
|
|
3400
|
+
const knowledgeTools = SIDECAR_KNOWLEDGE_TOOLS;
|
|
3401
|
+
if (knowledgeTools.includes(m.name) && this.opts.knowledge) {
|
|
3402
|
+
const off = this.knowledgeOffNow(this.opts.knowledge);
|
|
3403
|
+
if (off !== null) {
|
|
3404
|
+
this.opts.log?.(`invoke ${m.name} refused: ${off}`);
|
|
3405
|
+
this.ch.send({ t: 'invoke-result', id: m.id, ok: true, result: off });
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
if (m.name === SIDECAR_KNOWLEDGE_WRITE) {
|
|
3410
|
+
const refusal = this.knowledgeWriteRefusal();
|
|
3411
|
+
if (refusal) {
|
|
3412
|
+
this.opts.log?.(`invoke ${m.name} refused: ${refusal}`);
|
|
3413
|
+
this.ch.send({ t: 'invoke-result', id: m.id, ok: true, result: refusal });
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3231
3417
|
/* MUTATIONS ARE LOUD, READS ARE NOT.
|
|
3232
3418
|
*
|
|
3233
3419
|
* A single agent run issues hundreds of `fs_read_file` / `fs_list_dir`
|
|
@@ -4240,6 +4426,166 @@ export class SidecarServer {
|
|
|
4240
4426
|
...(this.verifiedUserId !== undefined ? { verifiedUserId: this.verifiedUserId } : {}),
|
|
4241
4427
|
};
|
|
4242
4428
|
}
|
|
4429
|
+
/** What a knowledge message answers when the port throws. The text of an
|
|
4430
|
+
* unexpected throw may name a path on this machine, so only a caller who
|
|
4431
|
+
* manages the folders (and so sees their paths) gets it; anyone else gets
|
|
4432
|
+
* KNOWLEDGE_FAILED, and the real text goes to the log. */
|
|
4433
|
+
knowledgeThrowText(kind, err) {
|
|
4434
|
+
const text = errMessage(err);
|
|
4435
|
+
if (this.knowledgeCanManage())
|
|
4436
|
+
return text;
|
|
4437
|
+
this.opts.log?.(`${kind} failed: ${text}`);
|
|
4438
|
+
return KNOWLEDGE_FAILED;
|
|
4439
|
+
}
|
|
4440
|
+
/** Why this sidecar's knowledge is off now, or null. A port whose answer
|
|
4441
|
+
* throws is treated as off, with a fixed sentence: fail closed. */
|
|
4442
|
+
knowledgeOffNow(port) {
|
|
4443
|
+
try {
|
|
4444
|
+
return port.off?.() ?? null;
|
|
4445
|
+
}
|
|
4446
|
+
catch (err) {
|
|
4447
|
+
this.opts.log?.(`knowledge: could not tell whether knowledge is on (${err instanceof Error ? err.message : String(err)})`);
|
|
4448
|
+
return KNOWLEDGE_NOT_KEPT;
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
knowledgeListReply(port) {
|
|
4452
|
+
const canManage = this.knowledgeCanManage();
|
|
4453
|
+
const folders = port.folders().map((f) => (canManage ? knowledgeViewForManager(f) : knowledgeViewForMember(f)));
|
|
4454
|
+
return { ok: true, folders, canManage };
|
|
4455
|
+
}
|
|
4456
|
+
onKnowledgeList(m) {
|
|
4457
|
+
if (typeof m.id !== 'number' || !Number.isSafeInteger(m.id))
|
|
4458
|
+
return;
|
|
4459
|
+
const answer = (reply) => this.ch.send({ t: 'knowledge-list-result', id: m.id, reply });
|
|
4460
|
+
const port = this.opts.knowledge;
|
|
4461
|
+
if (!port) {
|
|
4462
|
+
answer({ ok: false, error: KNOWLEDGE_NOT_KEPT });
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
const off = this.knowledgeOffNow(port);
|
|
4466
|
+
if (off !== null) {
|
|
4467
|
+
answer({ ok: false, error: off });
|
|
4468
|
+
return;
|
|
4469
|
+
}
|
|
4470
|
+
try {
|
|
4471
|
+
answer(this.knowledgeListReply(port));
|
|
4472
|
+
}
|
|
4473
|
+
catch (err) {
|
|
4474
|
+
answer({ ok: false, error: this.knowledgeThrowText('knowledge-list', err) });
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
/** Add or remove one knowledge folder. Refused unless this connection
|
|
4478
|
+
* manages the folders, before anything else in the message is looked at. */
|
|
4479
|
+
async onKnowledgeSet(m) {
|
|
4480
|
+
if (typeof m.id !== 'number' || !Number.isSafeInteger(m.id))
|
|
4481
|
+
return;
|
|
4482
|
+
const answer = (reply) => this.ch.send({ t: 'knowledge-set-result', id: m.id, reply });
|
|
4483
|
+
const port = this.opts.knowledge;
|
|
4484
|
+
if (!port) {
|
|
4485
|
+
answer({ ok: false, error: KNOWLEDGE_NOT_KEPT });
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4488
|
+
const off = this.knowledgeOffNow(port);
|
|
4489
|
+
if (off !== null) {
|
|
4490
|
+
answer({ ok: false, error: off });
|
|
4491
|
+
return;
|
|
4492
|
+
}
|
|
4493
|
+
if (!this.knowledgeCanManage()) {
|
|
4494
|
+
this.opts.log?.(`knowledge-set refused: ${KNOWLEDGE_ADMIN_ONLY}`);
|
|
4495
|
+
answer({ ok: false, error: KNOWLEDGE_ADMIN_ONLY });
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4498
|
+
const add = m.add !== undefined ? knowledgeAddOf(m.add) : undefined;
|
|
4499
|
+
const remove = typeof m.remove === 'string' ? m.remove : undefined;
|
|
4500
|
+
const oneOf = (m.add !== undefined) !== (m.remove !== undefined);
|
|
4501
|
+
if (!oneOf || add === null || (m.remove !== undefined && remove === undefined)) {
|
|
4502
|
+
answer({ ok: false, error: KNOWLEDGE_SET_SHAPE });
|
|
4503
|
+
return;
|
|
4504
|
+
}
|
|
4505
|
+
try {
|
|
4506
|
+
const done = await port.set(add ? { add } : { remove });
|
|
4507
|
+
if (!done.ok) {
|
|
4508
|
+
answer(done);
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4511
|
+
this.opts.log?.(add ? `knowledge folder added: ${add.path}` : `knowledge folder removed: ${remove ?? ''}`);
|
|
4512
|
+
answer(this.knowledgeListReply(port));
|
|
4513
|
+
}
|
|
4514
|
+
catch (err) {
|
|
4515
|
+
answer({ ok: false, error: this.knowledgeThrowText('knowledge-set', err) });
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
async onKnowledgeFiles(m) {
|
|
4519
|
+
if (typeof m.id !== 'number' || !Number.isSafeInteger(m.id))
|
|
4520
|
+
return;
|
|
4521
|
+
const answer = (reply) => this.ch.send({ t: 'knowledge-files-result', id: m.id, reply });
|
|
4522
|
+
const port = this.opts.knowledge;
|
|
4523
|
+
if (!port) {
|
|
4524
|
+
answer({ ok: false, error: KNOWLEDGE_NOT_KEPT });
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
const off = this.knowledgeOffNow(port);
|
|
4528
|
+
if (off !== null) {
|
|
4529
|
+
answer({ ok: false, error: off });
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
4532
|
+
if (typeof m.folder !== 'string') {
|
|
4533
|
+
answer({ ok: false, error: 'Name a knowledge folder by its id.' });
|
|
4534
|
+
return;
|
|
4535
|
+
}
|
|
4536
|
+
try {
|
|
4537
|
+
answer(await port.files(m.folder));
|
|
4538
|
+
}
|
|
4539
|
+
catch (err) {
|
|
4540
|
+
answer({ ok: false, error: this.knowledgeThrowText('knowledge-files', err) });
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
async onKnowledgeFile(m) {
|
|
4544
|
+
if (typeof m.id !== 'number' || !Number.isSafeInteger(m.id))
|
|
4545
|
+
return;
|
|
4546
|
+
const answer = (reply) => this.ch.send({ t: 'knowledge-file-result', id: m.id, reply });
|
|
4547
|
+
const port = this.opts.knowledge;
|
|
4548
|
+
if (!port) {
|
|
4549
|
+
answer({ ok: false, error: KNOWLEDGE_NOT_KEPT });
|
|
4550
|
+
return;
|
|
4551
|
+
}
|
|
4552
|
+
const off = this.knowledgeOffNow(port);
|
|
4553
|
+
if (off !== null) {
|
|
4554
|
+
answer({ ok: false, error: off });
|
|
4555
|
+
return;
|
|
4556
|
+
}
|
|
4557
|
+
if (typeof m.folder !== 'string' || typeof m.path !== 'string' || (m.stat !== undefined && typeof m.stat !== 'boolean')) {
|
|
4558
|
+
answer({ ok: false, error: 'Name a knowledge folder by its id and a file by its path in it.' });
|
|
4559
|
+
return;
|
|
4560
|
+
}
|
|
4561
|
+
try {
|
|
4562
|
+
answer(await port.file(m.folder, m.path, m.stat === true));
|
|
4563
|
+
}
|
|
4564
|
+
catch (err) {
|
|
4565
|
+
answer({ ok: false, error: this.knowledgeThrowText('knowledge-file', err) });
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
async onKnowledgeChanges(m) {
|
|
4569
|
+
if (typeof m.id !== 'number' || !Number.isSafeInteger(m.id))
|
|
4570
|
+
return;
|
|
4571
|
+
const answer = (reply) => this.ch.send({ t: 'knowledge-changes-result', id: m.id, reply });
|
|
4572
|
+
const port = this.opts.knowledge;
|
|
4573
|
+
if (!port) {
|
|
4574
|
+
answer({ ok: false, error: KNOWLEDGE_NOT_KEPT });
|
|
4575
|
+
return;
|
|
4576
|
+
}
|
|
4577
|
+
const off = this.knowledgeOffNow(port);
|
|
4578
|
+
if (off !== null) {
|
|
4579
|
+
answer({ ok: false, error: off });
|
|
4580
|
+
return;
|
|
4581
|
+
}
|
|
4582
|
+
try {
|
|
4583
|
+
answer(await port.changes());
|
|
4584
|
+
}
|
|
4585
|
+
catch (err) {
|
|
4586
|
+
answer({ ok: false, error: this.knowledgeThrowText('knowledge-changes', err) });
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4243
4589
|
async onFleetItems(m) {
|
|
4244
4590
|
if (typeof m.id !== 'number' || !Number.isSafeInteger(m.id))
|
|
4245
4591
|
return;
|
|
@@ -6116,6 +6462,41 @@ export class SidecarServer {
|
|
|
6116
6462
|
return MCP_ADMIN_ONLY;
|
|
6117
6463
|
}
|
|
6118
6464
|
}
|
|
6465
|
+
/** Team mode: running a command on the daemon's machine, or reaching one
|
|
6466
|
+
* that is running, is the admin's. Null when THIS connection may. Unknown
|
|
6467
|
+
* admin and any throw while deciding are refusals. Solo mode: always null. */
|
|
6468
|
+
commandRefusal() {
|
|
6469
|
+
try {
|
|
6470
|
+
const access = this.teamAccess();
|
|
6471
|
+
if (!access)
|
|
6472
|
+
return null;
|
|
6473
|
+
return sameAdmin(this.verifiedUserId, access.adminId()) ? null : COMMANDS_ADMIN_ONLY;
|
|
6474
|
+
}
|
|
6475
|
+
catch {
|
|
6476
|
+
return COMMANDS_ADMIN_ONLY;
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6479
|
+
/** Team mode: writing into this sidecar's knowledge folders is the admin's.
|
|
6480
|
+
* Null when THIS connection may. Unknown admin and any throw while deciding
|
|
6481
|
+
* are refusals. Solo mode: always null. */
|
|
6482
|
+
knowledgeWriteRefusal() {
|
|
6483
|
+
return this.knowledgeCanManage() ? null : KNOWLEDGE_WRITE_ADMIN_ONLY;
|
|
6484
|
+
}
|
|
6485
|
+
/** Whether THIS connection manages this sidecar's knowledge folders (sees
|
|
6486
|
+
* their paths, adds and removes them, writes into the writable one): always
|
|
6487
|
+
* on a solo daemon; on a team daemon only its admin. Unknown admin and any
|
|
6488
|
+
* throw while deciding are a no. */
|
|
6489
|
+
knowledgeCanManage() {
|
|
6490
|
+
try {
|
|
6491
|
+
const access = this.teamAccess();
|
|
6492
|
+
if (!access)
|
|
6493
|
+
return true;
|
|
6494
|
+
return sameAdmin(this.verifiedUserId, access.adminId());
|
|
6495
|
+
}
|
|
6496
|
+
catch {
|
|
6497
|
+
return false;
|
|
6498
|
+
}
|
|
6499
|
+
}
|
|
6119
6500
|
async onRoomFindings(m) {
|
|
6120
6501
|
if (!this.opts.rooms) {
|
|
6121
6502
|
// A daemon without the capability hosts no rooms, so it holds no
|
|
@@ -6168,8 +6549,12 @@ export class SidecarServer {
|
|
|
6168
6549
|
this.ch.send({ t: 'config-result', id: m.id, ok: false, error: gate.refusal });
|
|
6169
6550
|
return;
|
|
6170
6551
|
}
|
|
6552
|
+
/* WHOSE global memory (`memory/`) this call reaches: the service maps
|
|
6553
|
+
* it, after its own allowlist check, from this scope. Every call passes
|
|
6554
|
+
* it, so no route reaches `memory/` without the decision. */
|
|
6555
|
+
const scope = this.configMemoryScope();
|
|
6171
6556
|
if (m.t === 'config-list') {
|
|
6172
|
-
const entries = await svc.list(m.path, m.depth);
|
|
6557
|
+
const entries = await svc.list(m.path, m.depth, scope);
|
|
6173
6558
|
const shown = await this.visibleConfigEntries(svc, m.path, entries);
|
|
6174
6559
|
this.ch.send(shown
|
|
6175
6560
|
? { t: 'config-result', id: m.id, ok: true, entries: shown }
|
|
@@ -6177,18 +6562,18 @@ export class SidecarServer {
|
|
|
6177
6562
|
return;
|
|
6178
6563
|
}
|
|
6179
6564
|
if (m.t === 'config-read') {
|
|
6180
|
-
this.ch.send({ t: 'config-result', id: m.id, ok: true, content: await svc.read(m.path) });
|
|
6565
|
+
this.ch.send({ t: 'config-result', id: m.id, ok: true, content: await svc.read(m.path, scope) });
|
|
6181
6566
|
return;
|
|
6182
6567
|
}
|
|
6183
6568
|
if (m.t === 'config-write')
|
|
6184
|
-
await svc.write(m.path, m.content);
|
|
6569
|
+
await svc.write(m.path, m.content, scope);
|
|
6185
6570
|
else if (m.t === 'config-append') {
|
|
6186
6571
|
if (!svc.append)
|
|
6187
6572
|
throw new Error('this sidecar cannot append to its harness config');
|
|
6188
|
-
await svc.append(m.path, m.content);
|
|
6573
|
+
await svc.append(m.path, m.content, scope);
|
|
6189
6574
|
}
|
|
6190
6575
|
else
|
|
6191
|
-
await svc.remove(m.path);
|
|
6576
|
+
await svc.remove(m.path, scope);
|
|
6192
6577
|
await this.afterConfigChange(gate);
|
|
6193
6578
|
this.ch.send({ t: 'config-result', id: m.id, ok: true });
|
|
6194
6579
|
}
|
|
@@ -6196,6 +6581,20 @@ export class SidecarServer {
|
|
|
6196
6581
|
this.ch.send({ t: 'config-result', id: m.id, ok: false, error: errMessage(e) });
|
|
6197
6582
|
}
|
|
6198
6583
|
}
|
|
6584
|
+
/** Whose global memory a config call on THIS connection reaches. A
|
|
6585
|
+
* personal daemon, and a team daemon's admin: the shared `memory/` itself
|
|
6586
|
+
* (undefined). Any other team connection: its verified user's own partition,
|
|
6587
|
+
* or none at all (null) when no user is verified. Throws only when the
|
|
6588
|
+
* access port throws; the caller then refuses the call. */
|
|
6589
|
+
configMemoryScope() {
|
|
6590
|
+
const access = this.teamAccess();
|
|
6591
|
+
if (!access)
|
|
6592
|
+
return undefined;
|
|
6593
|
+
const user = this.verifiedUserId;
|
|
6594
|
+
if (sameAdmin(user, access.adminId()))
|
|
6595
|
+
return undefined;
|
|
6596
|
+
return { memoryUser: typeof user === 'string' && user !== '' ? user : null };
|
|
6597
|
+
}
|
|
6199
6598
|
/** The config path as the filesystem would reach it, for classification:
|
|
6200
6599
|
* separators, `.`, empty and `..` segments resolved; the `crew` segment, the
|
|
6201
6600
|
* agent folder (to its on-disk name, read from the service's own listing of
|