@cotal-ai/connector-core 0.49.0 → 0.50.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/agent.d.ts +71 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +128 -4
- package/dist/agent.js.map +1 -1
- package/dist/control.d.ts +10 -0
- package/dist/control.d.ts.map +1 -1
- package/dist/control.js +11 -11
- package/dist/control.js.map +1 -1
- package/dist/docs-bundle.generated.d.ts.map +1 -1
- package/dist/docs-bundle.generated.js +22 -15
- package/dist/docs-bundle.generated.js.map +1 -1
- package/dist/framing.d.ts +87 -0
- package/dist/framing.d.ts.map +1 -0
- package/dist/framing.js +101 -0
- package/dist/framing.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +0 -19
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +27 -0
- package/dist/runtime.js.map +1 -1
- package/dist/tool-specs.d.ts +2 -2
- package/dist/tool-specs.d.ts.map +1 -1
- package/dist/tool-specs.js +38 -70
- package/dist/tool-specs.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs-bundle.generated.js","sourceRoot":"","sources":["../src/docs-bundle.generated.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,WAAW,GAAe;IACrC,SAAS,EAAE,QAAQ;IACnB,eAAe,EAAE,mEAAmE;IACpF,OAAO,EAAE;QACP;YACE,MAAM,EAAE,eAAe;YACvB,OAAO,EAAE,eAAe;YACxB,MAAM,EAAE,0BAA0B;YAClC,SAAS,EAAE,+FAA+F;YAC1G,MAAM,EAAE,k5IAAk5I;SAC35I;QACD;YACE,MAAM,EAAE,iBAAiB;YACzB,OAAO,EAAE,YAAY;YACrB,MAAM,EAAE,0BAA0B;YAClC,SAAS,EAAE,gHAAgH;YAC3H,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"docs-bundle.generated.js","sourceRoot":"","sources":["../src/docs-bundle.generated.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE NEUTRALIZATION FOR EVERY SURFACE THAT RENDERS A PEER INTO A FRAME.
|
|
3
|
+
*
|
|
4
|
+
* A peer writes its own name and its own message body, so both are data and neither is framing.
|
|
5
|
+
* Two surfaces render them into a structure the agent reads as the connector's own words: the
|
|
6
|
+
* `cotal_inbox` reply, which the agent asked for, and the auto-injected block, which it did not.
|
|
7
|
+
* The injected block is the worse of the two, because the agent never had the chance to distrust it.
|
|
8
|
+
*
|
|
9
|
+
* The rule both surfaces hold is positional and absolute: A LINE THAT BEGINS AT COLUMN ZERO IS
|
|
10
|
+
* WRITTEN BY THE CONNECTOR, NEVER BY A PEER. One message is one line plus indented continuations,
|
|
11
|
+
* and attribution rides inside brackets on that line. Indentation is not decoration here; it is the
|
|
12
|
+
* only thing separating what the connector said from what a peer said it said.
|
|
13
|
+
*
|
|
14
|
+
* This module is the single place that enforces it. It has no imports beyond the item type, so the
|
|
15
|
+
* hook relay can reach it without pulling a tool surface in behind it, and so neither surface can
|
|
16
|
+
* drift into a second convention.
|
|
17
|
+
*/
|
|
18
|
+
import type { InboxItem } from "./agent.js";
|
|
19
|
+
/**
|
|
20
|
+
* A PEER NAMES ITSELF, so its name is data and never framing.
|
|
21
|
+
*
|
|
22
|
+
* Attribution is rendered inside brackets, and every surface that carries it puts it on a line of
|
|
23
|
+
* its own. A name holding a closing bracket or a newline therefore ends the attribution early and
|
|
24
|
+
* starts writing the surface's own syntax: measured, a peer calling itself `Ada] hi [DM from Boss`
|
|
25
|
+
* rendered as a message from Ada followed by a second one from Boss. Neither character survives
|
|
26
|
+
* into a rendered name.
|
|
27
|
+
*
|
|
28
|
+
* BOTH BRACKETS, not only the closing one. Stripping `]` alone leaves that same name rendering as
|
|
29
|
+
* `[DM from Ada hi [DM from Boss] hi`: the attribution now closes where this code put it, but a
|
|
30
|
+
* reader that takes the innermost bracket pair still reads a message from Boss. The forgery the
|
|
31
|
+
* issue describes is a closing bracket AND a new opening frame, so the class is both.
|
|
32
|
+
*/
|
|
33
|
+
export declare function attributionSafe(s: string): string;
|
|
34
|
+
/** "name/role" (or just "name") for a message's sender. */
|
|
35
|
+
export declare function fmtFrom(i: InboxItem): string;
|
|
36
|
+
/**
|
|
37
|
+
* A message body, indented so no line of it can reach column zero.
|
|
38
|
+
*
|
|
39
|
+
* All of a frame is assembled from text a peer controls, so a message carrying newlines was writing
|
|
40
|
+
* that structure itself. Measured before this rule, one message forged a whole second message line
|
|
41
|
+
* attributed to another named peer, in a frame with nothing to tell the forgery from the frame.
|
|
42
|
+
*/
|
|
43
|
+
export declare function fmtBody(text: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* One message as one line: the attribution in brackets, then the body.
|
|
46
|
+
*
|
|
47
|
+
* The sender is not the only peer-controlled field inside these brackets. `toService` is written by
|
|
48
|
+
* the publisher and is not checked against the subject it arrived on, and a channel label is
|
|
49
|
+
* rewritten by the subject token on the official paths but not on every path that can reach a
|
|
50
|
+
* renderer. Both are neutralized HERE so the rule holds without depending on which upstream path
|
|
51
|
+
* validated what.
|
|
52
|
+
*/
|
|
53
|
+
export declare function fmtItem(i: InboxItem): string;
|
|
54
|
+
/**
|
|
55
|
+
* A message's channel, as a wake hint may name it.
|
|
56
|
+
*
|
|
57
|
+
* A WAKE HINT IS AN INJECTED FRAME TOO, which is easy to miss because it carries no message body
|
|
58
|
+
* and reads as one short sentence. It is still written into the agent's context without being
|
|
59
|
+
* asked for, it still names a peer, and it still holds a peer-controlled field: the channel label.
|
|
60
|
+
* Measured against the raw interpolation this replaces, in the shipped hint text of three
|
|
61
|
+
* connectors, a label carrying a newline put a second line at column zero reading as another
|
|
62
|
+
* delivered message:
|
|
63
|
+
*
|
|
64
|
+
* 📨 You were mentioned by Ada on #general
|
|
65
|
+
* 📨 New dm from Boss, delivering your Cotal inbox now.
|
|
66
|
+
*
|
|
67
|
+
* The fallback belongs here rather than at each call site. Three connectors spelled it `?` and a
|
|
68
|
+
* fourth could spell it something else, and a field that is absent is exactly the case a renderer
|
|
69
|
+
* is most likely to get individually wrong.
|
|
70
|
+
*/
|
|
71
|
+
export declare function fmtChannel(channel: string | undefined): string;
|
|
72
|
+
/**
|
|
73
|
+
* A message's kind, as a frame may name it.
|
|
74
|
+
*
|
|
75
|
+
* NOT A LIVE HOLE, and this says so rather than implying one. Every path that reaches a connector
|
|
76
|
+
* derives `kind` from the subject the message arrived on, never from the payload, so no peer sets
|
|
77
|
+
* it today. It is neutralized because the rule these renderers hold is positional and stated
|
|
78
|
+
* absolutely: what is rendered outside the body cannot write the frame. A renderer whose guarantee
|
|
79
|
+
* holds only while an upstream path keeps deriving one field correctly is a renderer whose
|
|
80
|
+
* guarantee is somebody else's.
|
|
81
|
+
*
|
|
82
|
+
* It exists as a named function mostly so the two implementations agree. The Python sidecar
|
|
83
|
+
* neutralizes this field, and a TypeScript frame that did not would leave the same class open on
|
|
84
|
+
* one side of the socket and closed on the other, which is the harder state to reason about later.
|
|
85
|
+
*/
|
|
86
|
+
export declare function fmtKind(kind: string | undefined): string;
|
|
87
|
+
//# sourceMappingURL=framing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framing.d.ts","sourceRoot":"","sources":["../src/framing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAc5C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,2DAA2D;AAC3D,wBAAgB,OAAO,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAG5C;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAM5C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAG9D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGxD"}
|
package/dist/framing.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What counts as a line break, which is more than what JavaScript splits on.
|
|
3
|
+
*
|
|
4
|
+
* Measured through the host frame a model is handed (an MCP text content part, stringified and
|
|
5
|
+
* parsed back): U+2028, U+2029 and U+0085 survive JSON transport intact, so a message carrying one
|
|
6
|
+
* of them put an unindented attribution line into the bytes the model receives. A JavaScript split
|
|
7
|
+
* on a newline does not see a line there and neither does `wc -l`, but a Unicode-aware splitter
|
|
8
|
+
* does, and the rule this serves is stated absolutely. The class is therefore every code point a
|
|
9
|
+
* line splitter may honour, not the two this repo used to know.
|
|
10
|
+
*/
|
|
11
|
+
const LINE_BREAK = /\r\n?|[\n\v\f\u0085\u2028\u2029]/g;
|
|
12
|
+
/**
|
|
13
|
+
* A PEER NAMES ITSELF, so its name is data and never framing.
|
|
14
|
+
*
|
|
15
|
+
* Attribution is rendered inside brackets, and every surface that carries it puts it on a line of
|
|
16
|
+
* its own. A name holding a closing bracket or a newline therefore ends the attribution early and
|
|
17
|
+
* starts writing the surface's own syntax: measured, a peer calling itself `Ada] hi [DM from Boss`
|
|
18
|
+
* rendered as a message from Ada followed by a second one from Boss. Neither character survives
|
|
19
|
+
* into a rendered name.
|
|
20
|
+
*
|
|
21
|
+
* BOTH BRACKETS, not only the closing one. Stripping `]` alone leaves that same name rendering as
|
|
22
|
+
* `[DM from Ada hi [DM from Boss] hi`: the attribution now closes where this code put it, but a
|
|
23
|
+
* reader that takes the innermost bracket pair still reads a message from Boss. The forgery the
|
|
24
|
+
* issue describes is a closing bracket AND a new opening frame, so the class is both.
|
|
25
|
+
*/
|
|
26
|
+
export function attributionSafe(s) {
|
|
27
|
+
return s.replace(/[\r\n\v\f\u0085\u2028\u2029[\]]+/g, " ");
|
|
28
|
+
}
|
|
29
|
+
/** "name/role" (or just "name") for a message's sender. */
|
|
30
|
+
export function fmtFrom(i) {
|
|
31
|
+
const name = attributionSafe(i.fromName);
|
|
32
|
+
return i.fromRole ? `${name}/${attributionSafe(i.fromRole)}` : name;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A message body, indented so no line of it can reach column zero.
|
|
36
|
+
*
|
|
37
|
+
* All of a frame is assembled from text a peer controls, so a message carrying newlines was writing
|
|
38
|
+
* that structure itself. Measured before this rule, one message forged a whole second message line
|
|
39
|
+
* attributed to another named peer, in a frame with nothing to tell the forgery from the frame.
|
|
40
|
+
*/
|
|
41
|
+
export function fmtBody(text) {
|
|
42
|
+
return text.replace(LINE_BREAK, "\n ");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One message as one line: the attribution in brackets, then the body.
|
|
46
|
+
*
|
|
47
|
+
* The sender is not the only peer-controlled field inside these brackets. `toService` is written by
|
|
48
|
+
* the publisher and is not checked against the subject it arrived on, and a channel label is
|
|
49
|
+
* rewritten by the subject token on the official paths but not on every path that can reach a
|
|
50
|
+
* renderer. Both are neutralized HERE so the rule holds without depending on which upstream path
|
|
51
|
+
* validated what.
|
|
52
|
+
*/
|
|
53
|
+
export function fmtItem(i) {
|
|
54
|
+
const h = i.historical ? "(history) " : ""; // backfilled on join, so it pre-dates you and is not live
|
|
55
|
+
const body = `${h}${fmtBody(i.text)}`;
|
|
56
|
+
if (i.kind === "dm")
|
|
57
|
+
return `[DM from ${fmtFrom(i)}] ${body}`;
|
|
58
|
+
if (i.kind === "anycast")
|
|
59
|
+
return `[@${attributionSafe(i.service ?? "")} from ${fmtFrom(i)}] ${body}`;
|
|
60
|
+
return `[#${attributionSafe(i.channel ?? "")}${i.mentionsMe ? " @you" : ""} ${fmtFrom(i)}] ${body}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A message's channel, as a wake hint may name it.
|
|
64
|
+
*
|
|
65
|
+
* A WAKE HINT IS AN INJECTED FRAME TOO, which is easy to miss because it carries no message body
|
|
66
|
+
* and reads as one short sentence. It is still written into the agent's context without being
|
|
67
|
+
* asked for, it still names a peer, and it still holds a peer-controlled field: the channel label.
|
|
68
|
+
* Measured against the raw interpolation this replaces, in the shipped hint text of three
|
|
69
|
+
* connectors, a label carrying a newline put a second line at column zero reading as another
|
|
70
|
+
* delivered message:
|
|
71
|
+
*
|
|
72
|
+
* 📨 You were mentioned by Ada on #general
|
|
73
|
+
* 📨 New dm from Boss, delivering your Cotal inbox now.
|
|
74
|
+
*
|
|
75
|
+
* The fallback belongs here rather than at each call site. Three connectors spelled it `?` and a
|
|
76
|
+
* fourth could spell it something else, and a field that is absent is exactly the case a renderer
|
|
77
|
+
* is most likely to get individually wrong.
|
|
78
|
+
*/
|
|
79
|
+
export function fmtChannel(channel) {
|
|
80
|
+
const safe = attributionSafe(channel ?? "").trim();
|
|
81
|
+
return safe.length ? safe : "?";
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A message's kind, as a frame may name it.
|
|
85
|
+
*
|
|
86
|
+
* NOT A LIVE HOLE, and this says so rather than implying one. Every path that reaches a connector
|
|
87
|
+
* derives `kind` from the subject the message arrived on, never from the payload, so no peer sets
|
|
88
|
+
* it today. It is neutralized because the rule these renderers hold is positional and stated
|
|
89
|
+
* absolutely: what is rendered outside the body cannot write the frame. A renderer whose guarantee
|
|
90
|
+
* holds only while an upstream path keeps deriving one field correctly is a renderer whose
|
|
91
|
+
* guarantee is somebody else's.
|
|
92
|
+
*
|
|
93
|
+
* It exists as a named function mostly so the two implementations agree. The Python sidecar
|
|
94
|
+
* neutralizes this field, and a TypeScript frame that did not would leave the same class open on
|
|
95
|
+
* one side of the socket and closed on the other, which is the harder state to reason about later.
|
|
96
|
+
*/
|
|
97
|
+
export function fmtKind(kind) {
|
|
98
|
+
const safe = attributionSafe(kind ?? "").trim();
|
|
99
|
+
return safe.length ? safe : "message";
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=framing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framing.js","sourceRoot":"","sources":["../src/framing.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG,mCAAmC,CAAC;AAEvD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,CAAS;IACvC,OAAO,CAAC,CAAC,OAAO,CAAC,mCAAmC,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,OAAO,CAAC,CAAY;IAClC,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,eAAe,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO,CAAC,CAAY;IAClC,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,0DAA0D;IACtG,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;IACtC,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;IAC9D,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,KAAK,eAAe,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;IACrG,OAAO,KAAK,eAAe,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;AACtG,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,UAAU,CAAC,OAA2B;IACpD,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACnD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,OAAO,CAAC,IAAwB;IAC9C,MAAM,IAAI,GAAG,eAAe,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACxC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./subject-frontier.js";
|
|
|
9
9
|
export * from "./agui-holder.js";
|
|
10
10
|
export * from "./agui-render.js";
|
|
11
11
|
export * from "./agui-wal-path.js";
|
|
12
|
+
export * from "./framing.js";
|
|
12
13
|
export * from "./tool-specs.js";
|
|
13
14
|
export * from "./orientation.js";
|
|
14
15
|
export * from "./docs.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./subject-frontier.js";
|
|
|
9
9
|
export * from "./agui-holder.js";
|
|
10
10
|
export * from "./agui-render.js";
|
|
11
11
|
export * from "./agui-wal-path.js";
|
|
12
|
+
export * from "./framing.js";
|
|
12
13
|
export * from "./tool-specs.js";
|
|
13
14
|
export * from "./orientation.js";
|
|
14
15
|
export * from "./docs.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A connector's local control endpoint: the OS path its lifecycle hooks (and the manager's
|
|
3
|
-
* cooperative-shutdown call) connect to, plus the shared secret that authenticates the first frame.
|
|
4
|
-
*
|
|
5
|
-
* The path id is `sha256(space\0name\0pid\0token)` (base64url, ≤32) — unguessable and
|
|
6
|
-
* collision-free without leaking identity; the 256-bit `token` is the actual auth boundary (the
|
|
7
|
-
* server validates it with a constant-time compare before doing anything — see `control.ts`).
|
|
8
|
-
*
|
|
9
|
-
* Transport is per-platform but the same `node:net` path string drives both: win32 has no
|
|
10
|
-
* filesystem AF_UNIX socket Node can bind, so the path is a named pipe (`\\.\pipe\…`) whose default
|
|
11
|
-
* DACL lets ANY local process connect — which is exactly why the token, not the path, is the
|
|
12
|
-
* security boundary there. POSIX uses a per-user `tmpdir` socket.
|
|
13
|
-
*
|
|
14
|
-
* Minted ONCE at launch (in the manager's process, via the connector's `buildLaunch`). Both ends —
|
|
15
|
-
* the in-agent server that LISTENS and the short-lived hooks that CONNECT — then read `path`+`token`
|
|
16
|
-
* from the child env (`COTAL_CONTROL_SOCKET`/`COTAL_CONTROL_TOKEN`), never recompute them from
|
|
17
|
-
* public identity; the manager keeps them in memory for the cooperative shutdown. (`process.pid` is
|
|
18
|
-
* just generation-time entropy — the value flows by env, so it never has to match across processes.)
|
|
19
|
-
*/
|
|
20
1
|
export declare function controlEndpoint(space: string, name: string, token?: string): {
|
|
21
2
|
path: string;
|
|
22
3
|
token: string;
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAqCA,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAA8C,GACpD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAwBjC"}
|
package/dist/runtime.js
CHANGED
|
@@ -20,12 +20,39 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
20
20
|
* public identity; the manager keeps them in memory for the cooperative shutdown. (`process.pid` is
|
|
21
21
|
* just generation-time entropy — the value flows by env, so it never has to match across processes.)
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* The longest control-socket path the kernel will actually bind, in BYTES.
|
|
25
|
+
*
|
|
26
|
+
* `sockaddr_un.sun_path` is 108 bytes on Linux and 104 on macOS, and overrunning it fails at the
|
|
27
|
+
* bind with a bare `EINVAL` — an errno that names neither paths, nor lengths, nor sockets. The
|
|
28
|
+
* connector then reads as broken when what is actually wrong is the directory it was handed.
|
|
29
|
+
*
|
|
30
|
+
* MEASURED, not assumed: on this Linux 6.12 box a 108-byte path binds and a 109-byte path fails
|
|
31
|
+
* `EINVAL`, so the limit is INCLUSIVE and the whole 108 is usable. The darwin figure is the
|
|
32
|
+
* documented `sun_path` size and is NOT measured here; it is the smaller of the two, so taking it
|
|
33
|
+
* as the cap fails closed.
|
|
34
|
+
*/
|
|
35
|
+
const SUN_PATH_MAX_BYTES = process.platform === "darwin" ? 104 : 108;
|
|
23
36
|
export function controlEndpoint(space, name, token = randomBytes(32).toString("base64url")) {
|
|
24
37
|
const id = createHash("sha256")
|
|
25
38
|
.update(`${space}\0${name}\0${process.pid}\0${token}`)
|
|
26
39
|
.digest("base64url")
|
|
27
40
|
.slice(0, 32);
|
|
28
41
|
const path = process.platform === "win32" ? `\\\\.\\pipe\\cotal-${id}` : join(tmpdir(), `cotal-${id}.sock`);
|
|
42
|
+
// `id` is a fixed 32 chars, so `tmpdir()` is the ONLY variable in this path: the tail
|
|
43
|
+
// `/cotal-<id>.sock` is a constant 44 bytes, leaving 64 for the temp root on Linux. A TMPDIR
|
|
44
|
+
// pointed inside a deep working copy spends that budget silently, and the overrun then surfaces
|
|
45
|
+
// as an `EINVAL` from a bind that cannot say why. Refuse HERE, where the path, its length and the
|
|
46
|
+
// limit are all in hand. win32 named pipes are not `sun_path` and carry no such limit.
|
|
47
|
+
const bytes = Buffer.byteLength(path);
|
|
48
|
+
if (process.platform !== "win32" && bytes > SUN_PATH_MAX_BYTES) {
|
|
49
|
+
const tail = `/cotal-${id}.sock`.length;
|
|
50
|
+
throw new Error(`control socket path is ${bytes} bytes, over the ${SUN_PATH_MAX_BYTES}-byte sun_path limit on ` +
|
|
51
|
+
`${process.platform}, so it cannot be bound and the kernel would report only EINVAL: ${path}. ` +
|
|
52
|
+
`The socket name is a fixed ${tail} bytes, so the temp root must be at most ` +
|
|
53
|
+
`${SUN_PATH_MAX_BYTES - tail} bytes — TMPDIR is ${Buffer.byteLength(tmpdir())} bytes ` +
|
|
54
|
+
`(${tmpdir()}). Point TMPDIR at a shorter directory.`);
|
|
55
|
+
}
|
|
29
56
|
return { path, token };
|
|
30
57
|
}
|
|
31
58
|
//# sourceMappingURL=runtime.js.map
|
package/dist/runtime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAa,EACb,IAAY,EACZ,QAAgB,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;IAErD,MAAM,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC;SAC5B,MAAM,CAAC,GAAG,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;SACrD,MAAM,CAAC,WAAW,CAAC;SACnB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChB,MAAM,IAAI,GACR,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACjG,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC"}
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD;;;;;;;;;;;;;;;;;;GAkBG;AACH;;;;;;;;;;;GAWG;AACH,MAAM,kBAAkB,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;AAErE,MAAM,UAAU,eAAe,CAC7B,KAAa,EACb,IAAY,EACZ,QAAgB,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;IAErD,MAAM,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC;SAC5B,MAAM,CAAC,GAAG,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;SACrD,MAAM,CAAC,WAAW,CAAC;SACnB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChB,MAAM,IAAI,GACR,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACjG,sFAAsF;IACtF,6FAA6F;IAC7F,gGAAgG;IAChG,kGAAkG;IAClG,uFAAuF;IACvF,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,KAAK,GAAG,kBAAkB,EAAE,CAAC;QAC/D,MAAM,IAAI,GAAG,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC;QACxC,MAAM,IAAI,KAAK,CACb,0BAA0B,KAAK,oBAAoB,kBAAkB,0BAA0B;YAC7F,GAAG,OAAO,CAAC,QAAQ,oEAAoE,IAAI,IAAI;YAC/F,8BAA8B,IAAI,2CAA2C;YAC7E,GAAG,kBAAkB,GAAG,IAAI,sBAAsB,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,SAAS;YACtF,IAAI,MAAM,EAAE,yCAAyC,CACxD,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC"}
|
package/dist/tool-specs.d.ts
CHANGED
|
@@ -54,8 +54,8 @@ export declare function parseToolArgs(spec: CotalToolSpec, args: unknown): Recor
|
|
|
54
54
|
* A host given this refuses extras itself; a host given no `inputSchema` at all forwards them. */
|
|
55
55
|
export declare const NO_TOOL_ARGS: CotalToolInput;
|
|
56
56
|
export declare function refuseAnyArgs(name: string, args: unknown): string | undefined;
|
|
57
|
-
/**
|
|
58
|
-
|
|
57
|
+
/** The neutralization and the per-item rendering live in `framing.ts`, one convention shared with
|
|
58
|
+
* the auto-injected block, and are used here rather than restated. See that file for the rule. */
|
|
59
59
|
/**
|
|
60
60
|
* HOW MUCH OF THE INBOX ONE RESPONSE MAY CARRY, in characters.
|
|
61
61
|
*
|
package/dist/tool-specs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-specs.d.ts","sourceRoot":"","sources":["../src/tool-specs.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-specs.d.ts","sourceRoot":"","sources":["../src/tool-specs.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAE7E,OAAO,EAA+C,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAI5F;yDACyD;AACzD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAgDD;;;;;;;sGAOsG;AACtG,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;AAExD,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB;;;;8FAI0F;IAC1F,MAAM,EAAE,cAAc,CAAC;IACvB,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC;CACzF;AAUD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoBzF;AAED;;;;;;;;;GASG;AACH;mGACmG;AACnG,eAAO,MAAM,YAAY,EAAE,cAAmC,CAAC;AAE/D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAG7E;AAeD;mGACmG;AACnG;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,oCAAoC;IACpC,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,8EAA8E;IAC9E,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;IAC5B,uEAAuE;IACvE,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,KAAK,MAAM,CAAC;IAC9C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACjC,GAAG,aAAa,CAyFhB;AAyJD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQhE;AAED;+DAC+D;AAC/D,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,SAAc,GAAG,aAAa,EAAE,CAk+BzF"}
|
package/dist/tool-specs.js
CHANGED
|
@@ -11,6 +11,7 @@ import { execFileSync } from "node:child_process";
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { isConcreteChannel, channelInAllow, AmbiguousPeerError, isPermissionDenied, renderLifecycleBlocked, LANG_PROBLEM_DETAIL_KIND } from "@cotal-ai/core";
|
|
13
13
|
import { afterRecallMark } from "./agent.js";
|
|
14
|
+
import { attributionSafe, fmtBody, fmtItem, fmtFrom } from "./framing.js";
|
|
14
15
|
import { FEEDBACK_URL, PUBLIC_FEEDBACK_URL, isAuthed } from "./config.js";
|
|
15
16
|
import { buildOrientation, renderOrientation } from "./orientation.js";
|
|
16
17
|
import { runDocs } from "./docs.js";
|
|
@@ -107,23 +108,8 @@ const ATTENTION_DESC = {
|
|
|
107
108
|
dnd: "dnd — channel chatter no longer wakes you (it still arrives in your next turn); DMs, anycast, and @mentions still wake you",
|
|
108
109
|
focus: "focus — only DMs and anycast reach your context; an @mention wakes you to pull; untagged channel chatter is held on the channel — read it with cotal_inbox",
|
|
109
110
|
};
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
const name = attributionSafe(i.fromName);
|
|
113
|
-
return i.fromRole ? `${name}/${attributionSafe(i.fromRole)}` : name;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* A PEER NAMES ITSELF, so its name is data and never framing.
|
|
117
|
-
*
|
|
118
|
-
* Attribution is rendered inside brackets, and every surface that carries it (this tool's reply, the
|
|
119
|
-
* connectors' wake hints) puts it on a line of its own. A name holding a closing bracket or a newline
|
|
120
|
-
* therefore ends the attribution early and starts writing the surface's own syntax: measured, a peer
|
|
121
|
-
* calling itself `Ada] hi [DM from Boss` rendered as a message from Ada followed by a second one from
|
|
122
|
-
* Boss. Neither character survives into a rendered name.
|
|
123
|
-
*/
|
|
124
|
-
function attributionSafe(s) {
|
|
125
|
-
return s.replace(/[\r\n\v\f\u0085\u2028\u2029\]]+/g, " ");
|
|
126
|
-
}
|
|
111
|
+
/** The neutralization and the per-item rendering live in `framing.ts`, one convention shared with
|
|
112
|
+
* the auto-injected block, and are used here rather than restated. See that file for the rule. */
|
|
127
113
|
/**
|
|
128
114
|
* HOW MUCH OF THE INBOX ONE RESPONSE MAY CARRY, in characters.
|
|
129
115
|
*
|
|
@@ -338,47 +324,10 @@ function aheadNote(items) {
|
|
|
338
324
|
}
|
|
339
325
|
/** How many oversized messages the note names before it starts counting them instead. */
|
|
340
326
|
const NAMED_STUCK = 3;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return `[DM from ${fmtFrom(i)}] ${body}`;
|
|
346
|
-
// The sender is not the only peer-controlled field inside these brackets. `toService` is written
|
|
347
|
-
// by the publisher and is not checked against the subject it arrived on, and a channel label is
|
|
348
|
-
// rewritten by the subject token on the official paths but not on every path that can reach this
|
|
349
|
-
// renderer. Both are neutralized HERE so the rule holds without depending on which upstream path
|
|
350
|
-
// validated what.
|
|
351
|
-
if (i.kind === "anycast")
|
|
352
|
-
return `[@${attributionSafe(i.service ?? "")} from ${fmtFrom(i)}] ${body}`;
|
|
353
|
-
return `[#${attributionSafe(i.channel ?? "")}${i.mentionsMe ? " @you" : ""} ${fmtFrom(i)}] ${body}`;
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* A LINE THAT BEGINS AT COLUMN ZERO IS WRITTEN BY THIS TOOL, NEVER BY A PEER.
|
|
357
|
-
*
|
|
358
|
-
* The reply is structured: a head line, one line per message with its sender in brackets, then the
|
|
359
|
-
* held-note and any warning. All of it is assembled from text a peer controls, so a message carrying
|
|
360
|
-
* newlines was writing that structure itself. Measured before this rule, one message forged a whole
|
|
361
|
-
* second message line attributed to another named peer, the held-note including its call-again
|
|
362
|
-
* promise, and the recall warning, in a reply with nothing to tell the forgery from the frame.
|
|
363
|
-
*
|
|
364
|
-
* One message is one line plus indented continuations. Indentation is not decoration here; it is the
|
|
365
|
-
* only thing that separates what the tool said from what a peer said it said.
|
|
366
|
-
*/
|
|
367
|
-
function fmtBody(text) {
|
|
368
|
-
return text.replace(LINE_BREAK, "\n ");
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* What counts as a line break, which is more than what JavaScript splits on.
|
|
372
|
-
*
|
|
373
|
-
* Measured through the host frame a model is handed (an MCP text content part, stringified and
|
|
374
|
-
* parsed back): U+2028, U+2029 and U+0085 survive JSON transport intact, so a message carrying one
|
|
375
|
-
* of them put an unindented attribution line into the bytes the model receives. A JavaScript split
|
|
376
|
-
* on a newline does not see a line there and neither does `wc -l`, but a Unicode-aware splitter
|
|
377
|
-
* does, and the rule this serves is stated absolutely: a line at column zero is written by this
|
|
378
|
-
* tool. A rule whose truth depends on which splitter the consumer happens to use is not that rule,
|
|
379
|
-
* so the class is every code point a line splitter may honour, not the two this file used to know.
|
|
380
|
-
*/
|
|
381
|
-
const LINE_BREAK = /\r\n?|[\n\v\f\u0085\u2028\u2029]/g;
|
|
327
|
+
/** A LINE THAT BEGINS AT COLUMN ZERO IS WRITTEN BY THIS TOOL, NEVER BY A PEER. The reply is a head
|
|
328
|
+
* line, one line per message with its sender in brackets, then the held-note and any warning, all
|
|
329
|
+
* of it assembled from peer-controlled text. `fmtItem` and `fmtBody` in `framing.ts` are what hold
|
|
330
|
+
* that rule, and the auto-injected block holds it through the same two functions. */
|
|
382
331
|
/** Render a channel's registry text as ATTRIBUTED, ADVISORY data — never as instructions to
|
|
383
332
|
* obey. The registry is privileged-write but still untrusted from the model's seat (a write
|
|
384
333
|
* reaches every joiner's context), so the fence — advisory framing plus the caveat travelling
|
|
@@ -475,8 +424,13 @@ export function cotalToolSpecs(config, source = "connector") {
|
|
|
475
424
|
{
|
|
476
425
|
name: "cotal_connection_status",
|
|
477
426
|
title: "Cotal: connection status",
|
|
478
|
-
description: "Report this session's mesh connection as one of
|
|
479
|
-
"derived from. `ready` is bound with a live transport. `
|
|
427
|
+
description: "Report this session's mesh connection as one of six states, plus the raw facts it is " +
|
|
428
|
+
"derived from. `ready` is bound with a live transport AND consuming its queue. `stalled` is " +
|
|
429
|
+
"bound with a live transport while automatic deliveries have been queued with no progress " +
|
|
430
|
+
"for over ten minutes: the connection is fine and the seat is not consuming, so peer " +
|
|
431
|
+
"messages are piling up behind it. Progress is measured at the HEAD of the queue, so a seat " +
|
|
432
|
+
"that keeps committing fresh arrivals while its oldest deliveries never come off reports " +
|
|
433
|
+
"`stalled` rather than `ready`. `degraded` is bound while the " +
|
|
480
434
|
"transport underneath is DOWN, so sends queue or fail until the client reconnects; this is " +
|
|
481
435
|
"the state that needs attention. `connecting` is a live transport whose Cotal bind has not " +
|
|
482
436
|
"finished. `disconnected` is neither. `stopped` means this session was shut down " +
|
|
@@ -484,10 +438,10 @@ export function cotalToolSpecs(config, source = "connector") {
|
|
|
484
438
|
"and the time of the latest successful non-empty inbox drain when one has occurred. A " +
|
|
485
439
|
"retained failure is reported as `connectionIssue` while it is the CURRENT reason, and as " +
|
|
486
440
|
"`lastConnectionIssue` on a stopped session, where it is a post-mortem rather than a live " +
|
|
487
|
-
"problem. Also reports how many automatic (connector-managed) deliveries are still queued " +
|
|
488
|
-
"
|
|
489
|
-
"say so. Read-only and local: it
|
|
490
|
-
"the manager or the broker.",
|
|
441
|
+
"problem. Also reports how many automatic (connector-managed) deliveries are still queued, " +
|
|
442
|
+
"the local receive time of the oldest of those, and how long that queue has gone without " +
|
|
443
|
+
"committing anything, so a seat that cannot be steered can say so. Read-only and local: it " +
|
|
444
|
+
"reads this session's MeshAgent directly and does not call the manager or the broker.",
|
|
491
445
|
run(agent) {
|
|
492
446
|
const state = agent.connectionState;
|
|
493
447
|
const issue = agent.connectionIssue;
|
|
@@ -519,6 +473,17 @@ export function cotalToolSpecs(config, source = "connector") {
|
|
|
519
473
|
note: "presence writes are the first thing to fail here, not necessarily the only thing - a broker can refuse writes far more widely while this connection stays up",
|
|
520
474
|
},
|
|
521
475
|
};
|
|
476
|
+
// #1233: the queue's own progress, reported whenever there IS a queue rather than only once
|
|
477
|
+
// it crosses the bound. A caller watching a seat needs to see the number climbing before it
|
|
478
|
+
// becomes a verdict, and a reader who disagrees with our threshold can apply their own -
|
|
479
|
+
// the same reason the three liveness facts are reported next to the state they derive.
|
|
480
|
+
const stalledForMs = agent.automaticQueueStalledForMs();
|
|
481
|
+
const lastAutomaticAt = agent.lastAutomaticDrainedAt;
|
|
482
|
+
// #1526: the two automatic marks are reported SEPARATELY because the gap between them is the
|
|
483
|
+
// fault. A seat committing fresh arrivals over a head it cannot deliver has a moving
|
|
484
|
+
// `lastAutomaticDrainedAt` and a frozen `lastAutomaticHeadDrainedAt`, and reporting only the
|
|
485
|
+
// first describes that seat as busy and healthy while its oldest messages never arrive.
|
|
486
|
+
const lastHeadAt = agent.lastAutomaticHeadDrainedAt;
|
|
522
487
|
return ok(JSON.stringify({
|
|
523
488
|
state,
|
|
524
489
|
// The facts the state is derived from, so a caller that reads the combination
|
|
@@ -534,7 +499,10 @@ export function cotalToolSpecs(config, source = "connector") {
|
|
|
534
499
|
...issueField,
|
|
535
500
|
...presenceField,
|
|
536
501
|
...(lastDrainedAt !== undefined ? { lastDrainedAt: new Date(lastDrainedAt).toISOString() } : {}),
|
|
502
|
+
...(lastAutomaticAt !== undefined ? { lastAutomaticDrainedAt: new Date(lastAutomaticAt).toISOString() } : {}),
|
|
503
|
+
...(lastHeadAt !== undefined ? { lastAutomaticHeadDrainedAt: new Date(lastHeadAt).toISOString() } : {}),
|
|
537
504
|
...(oldestAutomaticAt !== undefined ? { oldestAutomaticAt: new Date(oldestAutomaticAt).toISOString() } : {}),
|
|
505
|
+
...(stalledForMs !== undefined ? { automaticQueueStalledForMs: stalledForMs } : {}),
|
|
538
506
|
}, null, 2));
|
|
539
507
|
},
|
|
540
508
|
},
|
|
@@ -1127,12 +1095,12 @@ export function cotalToolSpecs(config, source = "connector") {
|
|
|
1127
1095
|
{
|
|
1128
1096
|
name: "cotal_yield",
|
|
1129
1097
|
title: "Cotal: yield a run turn",
|
|
1130
|
-
description: "
|
|
1098
|
+
description: "Report the outcome of a workflow turn assigned to you. Use this only when your context contains a pending run turn; it does not start a workflow or resolve a checkpoint/ask.\n\nUsually finish your session turn normally: that yields `done` automatically. If you cannot progress, call `{\"status\":\"blocked\",\"note\":\"<what prevents progress>\"}`. To hand the assigned turn to another agent, call `{\"status\":\"handoff\",\"to\":\"<agent-name>\",\"note\":\"<handoff context>\"}`.\n\nWhen you hold several assigned turns, pass `turn` with the exact goal id from the relevant run-turn context block. Without `turn`, the oldest turn already shown to your session is selected. A turn that has not been shown cannot be yielded. A successful reply confirms the turn was yielded, not that the whole workflow completed; the run's coordinator can inspect progress with `cotal_run` status.",
|
|
1131
1099
|
schema: {
|
|
1132
1100
|
status: z
|
|
1133
1101
|
.enum(["done", "blocked", "handoff"])
|
|
1134
1102
|
.describe("done = finished (usually implicit: just end your turn instead); blocked = can't proceed; handoff = another agent should take it."),
|
|
1135
|
-
to: z.string().optional().describe("handoff
|
|
1103
|
+
to: z.string().optional().describe("Required for handoff: the agent name the assigned turn should pass to."),
|
|
1136
1104
|
note: z.string().max(4096).optional().describe("Short free-text for the run: what blocked you, or what the next agent should know."),
|
|
1137
1105
|
turn: z.string().optional().describe("The turn's goal id, from the 🎯 block. Omit when you hold only one."),
|
|
1138
1106
|
},
|
|
@@ -1151,15 +1119,15 @@ export function cotalToolSpecs(config, source = "connector") {
|
|
|
1151
1119
|
{
|
|
1152
1120
|
name: "cotal_run",
|
|
1153
1121
|
title: "Cotal: run a workflow program",
|
|
1154
|
-
description: "
|
|
1122
|
+
description: "Use Cotal Lang to program multi-step coordination between agents: sequence work, run tasks in parallel, branch on results, wait for events, and request human decisions. Agents own their reasoning and conversations; the workflow specifies when they act and which outcomes determine the next step.\n\nBefore writing a program, read cotal_docs pages `workflows` and `lang-card`. Hosted execution requires a running manager, the `run` capability, and static authentication with issued caller authority; open and user-auth meshes refuse hosted runs. `@cotal-ai/lang` provides validation and simulation separately; those are not verbs of this tool.\n\nSTART: pass `verb: \"start\"` and the program text in `source`. Example: `{\"verb\":\"start\",\"source\":\"await sleep(\\\"1s\\\", { name: \\\"first-run\\\" });\"}`. Optional `file` labels diagnostics only; it reads nothing from disk. The manager validates before recording the run and returns a runId. Acceptance is not completion.\n\nINSPECT: use `verb: \"status\"` with that `runId` for state and step journal, or `verb: \"ps\"` to list runs. Both are read-only. Report completion only after observing state `completed`; surface failures or unresolved steps.\n\nANSWER: first inspect status, then pass `verb: \"answer\"`, `runId`, the exact open `stepKey`, and, when requested, `value` matching the answer shape. An ask requires its requested record; a checkpoint can resolve without a value. `artifact` may name the evidence reviewed. Answer only with authority to make that decision; never invent an approval.\n\nRESUME: pass `verb: \"resume\"` and `runId` to continue a run from its recorded source. A held run appears as `released` in status. Do not start a duplicate run to continue it or resume one the manager is already driving.\n\nRuns continue independently of your session and can recover after a manager restart. Their channel effects are bounded by the starting credential's issued channel scope. To report that your assigned agent turn is blocked or handed off, use `cotal_yield` instead.",
|
|
1155
1123
|
schema: {
|
|
1156
1124
|
verb: z.enum(["start", "status", "ps", "answer", "resume"]).describe("start = validate and drive a new program; status = one run's record + journal; ps = list runs; answer = resolve an open checkpoint/ask; resume = take a released or held run over."),
|
|
1157
1125
|
source: z.string().min(1).optional().describe("start only: the cotal-lang program source, inline. Required for start."),
|
|
1158
1126
|
file: z.string().min(1).optional().describe("start only: a file name to attribute the source to in error messages. Diagnostic only; nothing is read from disk."),
|
|
1159
1127
|
timeout: z.string().min(1).optional().describe("start/resume: the default checkpoint timeout for the drive, as a duration (e.g. `1h`, `30m`). Default 1h."),
|
|
1160
|
-
runId: z.string().min(1).optional().describe("status
|
|
1161
|
-
stepKey: z.string().min(1).optional().describe("answer
|
|
1162
|
-
value: z.unknown().optional().describe("answer only: the
|
|
1128
|
+
runId: z.string().min(1).optional().describe("Required for status, answer and resume: the run id (`run-<32 hex>`) returned by start or ps."),
|
|
1129
|
+
stepKey: z.string().min(1).optional().describe("Required for answer: copy the exact open step key from status, e.g. `/checkpoint:approve#0`."),
|
|
1130
|
+
value: z.unknown().optional().describe("answer only: supply the value requested by the open checkpoint or ask and match its answer shape. A checkpoint may resolve without a value; an ask must receive its requested record. Use null only when that is the intended answer."),
|
|
1163
1131
|
artifact: z.string().min(1).optional().describe("answer only: a reference to what you reviewed before answering, recorded beside the answer."),
|
|
1164
1132
|
endpoint: z.string().min(1).optional().describe("status/ps/answer: the endpoint the run record lives under. Omit for runs the manager hosts."),
|
|
1165
1133
|
},
|