@chatpanel/events 0.101.0 → 0.103.1

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 (138) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +1 -4
  62. package/pdf-layout.js +7 -253
  63. package/project-run.js +20 -444
  64. package/project.js +1 -170
  65. package/promotion.js +1 -171
  66. package/queue.js +1 -98
  67. package/reach.js +1 -23
  68. package/recipe-tool.js +2 -161
  69. package/recipe.js +1 -264
  70. package/record-list.js +2 -110
  71. package/recruit.js +3 -456
  72. package/redaction-tokens.js +1 -61
  73. package/ref.js +1 -55
  74. package/registry.js +1 -240
  75. package/route-graph.js +1 -115
  76. package/route-strategies.js +2 -232
  77. package/router.js +2 -880
  78. package/rrf.js +1 -89
  79. package/rules.js +1 -142
  80. package/schedule.js +1 -663
  81. package/scm-connection.js +1 -180
  82. package/scopes.js +1 -12
  83. package/scorecard.js +1 -335
  84. package/search-engines.js +1 -81
  85. package/skill-manifest.js +1 -330
  86. package/skill-scan.js +3 -209
  87. package/skill-sources.js +1 -162
  88. package/skill-vars.js +2 -252
  89. package/slash-commands.js +1 -158
  90. package/source-gate.js +1 -92
  91. package/sources-retrieval.js +5 -186
  92. package/sources.js +1 -273
  93. package/store.js +1 -171
  94. package/structured.js +4 -901
  95. package/subject-kinds.js +1 -5
  96. package/subject-name.js +1 -96
  97. package/sync-plan.js +1 -213
  98. package/synthesis.js +3 -123
  99. package/tags.js +1 -203
  100. package/team-board.js +6 -337
  101. package/team-cache.js +2 -63
  102. package/team-observe.js +1 -118
  103. package/team-org.js +1 -348
  104. package/team-plan.js +4 -131
  105. package/team-record.js +1 -225
  106. package/team-run.js +9 -652
  107. package/team-subtask.js +2 -191
  108. package/team-task.js +2 -124
  109. package/team-tool.js +2 -196
  110. package/team-trail.js +2 -73
  111. package/team-worklog.js +2 -149
  112. package/team.js +1 -302
  113. package/text-search.js +1 -155
  114. package/theme.js +17 -154
  115. package/titles.js +4 -285
  116. package/tool-discovery.js +1 -87
  117. package/tool-dispatch.js +5 -230
  118. package/tool-groups.js +1 -81
  119. package/tool-hints.js +10 -185
  120. package/tool-loop-guard.js +4 -182
  121. package/tool-need.js +2 -124
  122. package/tool-result.js +2 -438
  123. package/tool-round.js +0 -0
  124. package/tool-schema.js +1 -155
  125. package/tool-traits.js +1 -182
  126. package/toolset.js +2 -88
  127. package/trajectory.js +1 -530
  128. package/turn-loop.js +4 -440
  129. package/upcast.js +1 -37
  130. package/vault.js +2 -250
  131. package/view.js +1 -88
  132. package/voice-intents.js +2 -1488
  133. package/voice-speaker.js +1 -98
  134. package/weather-tool.js +1 -65
  135. package/weather.js +2 -211
  136. package/web-search-tool.js +14 -102
  137. package/web-search.js +1 -160
  138. package/widget.js +1 -153
package/source-gate.js CHANGED
@@ -1,92 +1 @@
1
- // The source ceiling would this turn send internal material to a model that is too far
2
- // away? — as a GATE, not a ranking.
3
- //
4
- // Routing is not enough. A router returns null in every uncertain case and null means
5
- // "leave the choice alone", which is right for a preference and catastrophic for a privacy
6
- // rule: an internal page with no local model available would have gone to the third party
7
- // the user had selected. And routing only runs under Auto, so a manually chosen cloud model
8
- // bypassed it entirely. So this runs on every turn, after routing has had its chance to
9
- // pick something local, and it REFUSES rather than substituting: silently answering from a
10
- // different model is the substitution this codebase keeps removing, and silently sending
11
- // anyway is the leak it exists to stop.
12
- //
13
- // Was the extension's `sourceGate` + `sourcePolicySettings` + `sourceUrlsOf`; the desktop
14
- // had none of it and sent internal pages wherever the picker pointed. The policy (which
15
- // hosts are internal, how far their content may travel) is the user's and travels in the
16
- // shared `internalSites` preference section, so both clients enforce the same rule.
17
- //
18
- // Class R: no I/O. The target's reach comes from `reachOf` (a bridge agent is 'trusted', a
19
- // localhost endpoint 'device', the rest 'any') unless the caller already knows it.
20
-
21
- import { sourcePolicyFor, sourceUrlsOf, DEFAULT_INTERNAL_PATTERNS } from './sources.js';
22
- import { reachOf } from './model-candidates.js';
23
- import { REACH, reachRank } from './reach.js';
24
-
25
- // `sourceUrlsOf` lives in sources.js (no router on its graph) so a client's first paint can
26
- // list a turn's addresses without pulling the router in; it is re-exported here for callers
27
- // that have the gate anyway.
28
- export { sourceUrlsOf };
29
-
30
- /**
31
- * The internal-source policy, read from the `privacy` settings object in ONE place.
32
- *
33
- * NEVER CONFIGURED and CONFIGURED TO NOTHING are different answers. Undefined means the
34
- * user has not been here yet, so the built-ins apply; an array — even an empty one — is a
35
- * list they edited, and prepending our own to it would make a default impossible to
36
- * remove. Someone testing against localhost has a real reason to delete that line.
37
- */
38
- export function sourcePolicySettings(privacy = {}) {
39
- const cfg = privacy || {};
40
- const saved = cfg.internalPatterns;
41
- const list = Array.isArray(saved)
42
- ? saved
43
- : (saved == null ? DEFAULT_INTERNAL_PATTERNS : String(saved).split(/[\s,]+/));
44
- return {
45
- enabled: cfg.internalGuard !== false,
46
- patterns: list.map((x) => String(x || '').trim().toLowerCase()).filter(Boolean),
47
- ceiling: cfg.internalCeiling === 'trusted' ? 'trusted' : 'device',
48
- };
49
- }
50
-
51
- /** What the sources of a turn allow. Null when the guard is off or nothing matched. */
52
- export function sourceGuardFor(policy, sources = []) {
53
- if (!policy || !policy.enabled || !sources?.length) return null;
54
- const p = sourcePolicyFor(sources, { patterns: policy.patterns, ceiling: policy.ceiling });
55
- return p.internal ? p : null;
56
- }
57
-
58
- /**
59
- * The gate. `{ blocked, why, reach, message }` when the target is out of reach for these
60
- * sources; null when the turn may go.
61
- *
62
- * @param policy `sourcePolicySettings(privacy)`
63
- * @param messages the conversation as it will be sent
64
- * @param sources anything else the caller knows was attached
65
- * @param target `{ kind, baseUrl }` (reach computed) or `{ reach }` (reach known)
66
- * @param label how to name the target in the message
67
- */
68
- export function sourceGate({ policy, messages = [], sources = [], target = {}, label = '' } = {}) {
69
- const guard = sourceGuardFor(policy, sourceUrlsOf(messages, sources));
70
- if (!guard) return null;
71
- const actual = REACH.includes(target?.reach) ? target.reach : reachOf(target || {});
72
- if (reachRank(actual) <= reachRank(guard.reach)) return null;
73
- const where = guard.reach === 'device' ? 'stay on this device' : 'stay inside your workspace';
74
- return {
75
- blocked: true,
76
- why: guard.why,
77
- reach: guard.reach,
78
- // Name the source, the model and the way out. A refusal a person cannot act on gets
79
- // switched off wholesale, which would leave them worse protected than before.
80
- message: `Not sent: ${guard.why}. "${label || 'this model'}" is outside that, and content from an internal source must ${where}. `
81
- + 'Pick a local model (or run one), or remove this site under Settings → Privacy → Internal sites.',
82
- };
83
- }
84
-
85
- /** Is this error the gate's refusal? A runner reads it as "this model, not this task". */
86
- export const isSourceGateError = (err) => /^Not sent: /.test(String(err?.message || err || ''));
87
-
88
- /** The candidates a turn under `guard` may be handed at all — for a roster, before appointment. */
89
- export function withinReach(candidates = [], guard = null) {
90
- if (!guard) return candidates;
91
- return (candidates || []).filter((c) => reachRank(REACH.includes(c?.reach) ? c.reach : reachOf(c || {})) <= reachRank(guard.reach));
92
- }
1
+ import{sourcePolicyFor as f,sourceUrlsOf as a,DEFAULT_INTERNAL_PATTERNS as d}from"./sources.js";import{reachOf as c}from"./model-candidates.js";import{REACH as l,reachRank as o}from"./reach.js";function w(e={}){const t=e||{},r=t.internalPatterns,i=Array.isArray(r)?r:r==null?d:String(r).split(/[\s,]+/);return{enabled:t.internalGuard!==!1,patterns:i.map(s=>String(s||"").trim().toLowerCase()).filter(Boolean),ceiling:t.internalCeiling==="trusted"?"trusted":"device"}}function m(e,t=[]){if(!e||!e.enabled||!t?.length)return null;const r=f(t,{patterns:e.patterns,ceiling:e.ceiling});return r.internal?r:null}function S({policy:e,messages:t=[],sources:r=[],target:i={},label:s=""}={}){const n=m(e,a(t,r));if(!n)return null;const u=l.includes(i?.reach)?i.reach:c(i||{});if(o(u)<=o(n.reach))return null;const h=n.reach==="device"?"stay on this device":"stay inside your workspace";return{blocked:!0,why:n.why,reach:n.reach,message:`Not sent: ${n.why}. "${s||"this model"}" is outside that, and content from an internal source must ${h}. Pick a local model (or run one), or remove this site under Settings → Privacy → Internal sites.`}}const v=e=>/^Not sent: /.test(String(e?.message||e||""));function x(e=[],t=null){return t?(e||[]).filter(r=>o(l.includes(r?.reach)?r.reach:c(r||{}))<=o(t.reach)):e}export{v as isSourceGateError,S as sourceGate,m as sourceGuardFor,w as sourcePolicySettings,a as sourceUrlsOf,x as withinReach};
@@ -1,189 +1,8 @@
1
- /**
2
- * SOURCES ARE QUERIED, NOT POURED IN.
3
- *
4
- * Attached context used to be flattened into the first message: every attached tab, in full,
5
- * before the model had said anything. So "hi" on a long page paid for the whole page, and
6
- * five attached tabs meant five documents in the prompt to answer a question that concerned
7
- * one paragraph of one of them.
8
- *
9
- * The model is instead shown a MANIFEST — what exists, how big it is, where it came from —
10
- * and pulls what it needs. Three things follow that are not just savings:
11
- *
12
- * - The turn starts small, so a cheap model can handle a greeting on a heavy page.
13
- * - What was read is a fact in the log rather than an assumption about the prompt.
14
- * - Retrieval takes a QUERY, so a large source returns the relevant part instead of its
15
- * first N characters — which is what truncation gives you, and it is rarely the part
16
- * that matters.
17
- *
18
- * Pure and dependency-free: the extension, the gateway and the bridge all have attached
19
- * sources, and three implementations of "what did the model actually read" would drift.
20
- */
1
+ const $=/\n{2,}/,d=i=>Math.ceil(String(i||"").length/4);function p(i,r=0){return`${String(i?.kind||"src").toLowerCase().replace(/[^a-z0-9]+/g,"")||"src"}-${r+1}`}function S(i=[]){const r=(Array.isArray(i)?i:[i]).filter(e=>e&&(e.text||e.url)).map((e,t)=>({id:e.id||p(e,t),kind:e.kind||"context",title:e.title||e.url||"Untitled",url:e.url||"",text:String(e.text||""),tokens:d(e.text)})),o=new Map(r.map(e=>[e.id,e]));return{entries:r,get:e=>o.get(String(e||"").trim())||null,get tokens(){return r.reduce((e,t)=>e+t.tokens,0)}}}function k(i,r=120){const o=String(i||"");if(!o||o.length<=r)return o;try{const e=new URL(o),t=`${e.host}${e.pathname}`.replace(/\/$/,""),c=[...e.searchParams.entries()][0],u=c&&`${c[0]}=${c[1]}`,a=u&&`${t}?${u}`,h=a&&a.length<=r?a:t;return h.length<=r?`${h}…`:`${h.slice(0,r)}…`}catch{return`${o.slice(0,r)}…`}}function b(i){return i?.entries?.length?["Attached sources (NOT included below — read them with the `source` tool):",...i.entries.map(o=>`- ${o.id} — ${o.title}${o.url?` (${k(o.url)})`:""} · ~${o.tokens} tokens`)].join(`
2
+ `):""}function w(i,r){if(!r.length)return 0;const o=i.toLowerCase();let e=0;for(const t of r)o.includes(t)&&(e+=Math.min(3,t.length/4));return e}const x=new Set(["the","and","for","this","that","with","from","what","when","where","which","have","has","was","are","you","your","can","about","into","does","did","how","why","all","any","its"]);function m(i){return String(i||"").toLowerCase().split(/[^a-z0-9]+/).filter(r=>r.length>2&&!x.has(r))}function y(i,{id:r,query:o="",maxTokens:e=2e3}={}){const t=i?.get?.(r);if(!t){const n=(i?.entries||[]).map(s=>s.id).join(", ");return{error:`No attached source '${r}'.${n?` Available: ${n}.`:" Nothing is attached."}`}}const c=Math.max(200,Number(e)||2e3);if(t.tokens<=c)return{id:t.id,title:t.title,url:t.url,text:t.text,truncated:!1,of:t.tokens};const u=m(o),a=t.text.split($).filter(n=>n.trim());if(!u.length){const n=t.text.slice(0,c*4);return{id:t.id,title:t.title,url:t.url,text:n,truncated:!0,of:t.tokens,note:`Showing the first ~${d(n)} of ${t.tokens} tokens. Pass a query to get the relevant parts instead.`}}const h=a.map((n,s)=>({c:n,i:s,s:w(n,u)})).filter(n=>n.s>0).sort((n,s)=>s.s-n.s),l=[];let f=0;for(const n of h){const s=d(n.c);f+s>c||(l.push(n),f+=s)}if(!l.length){const n=t.text.slice(0,c*4);return{id:t.id,title:t.title,url:t.url,text:n,truncated:!0,of:t.tokens,note:`Nothing in this source matched '${o}'. Showing the beginning instead.`}}l.sort((n,s)=>n.i-s.i);const g=l.some((n,s)=>s>0&&n.i!==l[s-1].i+1);return{id:t.id,title:t.title,url:t.url,text:l.map(n=>n.c).join(g?`
21
3
 
22
- const CHUNK_SPLIT = /\n{2,}/;
4
+ […]
23
5
 
24
- /** ~4 chars per token — the same estimate the dispatcher budget uses. One rough number beats two. */
25
- export const approxTokens = (s) => Math.ceil(String(s || '').length / 4);
6
+ `:`
26
7
 
27
- /**
28
- * A stable, short id for a source. The model has to type this back, so it is derived from the
29
- * title rather than from a random string: `page-2` is recoverable from a manifest a model
30
- * half-remembers in a way that `k3f9a1` is not.
31
- */
32
- export function sourceId(source, index = 0) {
33
- const base = String(source?.kind || 'src').toLowerCase().replace(/[^a-z0-9]+/g, '') || 'src';
34
- return `${base}-${index + 1}`;
35
- }
36
-
37
- /**
38
- * Build the manifest the model sees INSTEAD of the content.
39
- *
40
- * Deliberately includes the size. A model that can see one source is 300 tokens and another
41
- * is 40,000 can decide to read the small one whole and query the large one — a decision it
42
- * cannot make when both are simply "a document".
43
- */
44
- export function makeSourceStore(sources = []) {
45
- const entries = (Array.isArray(sources) ? sources : [sources])
46
- .filter((s) => s && (s.text || s.url))
47
- .map((s, i) => ({
48
- id: s.id || sourceId(s, i),
49
- kind: s.kind || 'context',
50
- title: s.title || s.url || 'Untitled',
51
- url: s.url || '',
52
- text: String(s.text || ''),
53
- tokens: approxTokens(s.text),
54
- }));
55
- const byId = new Map(entries.map((e) => [e.id, e]));
56
- return {
57
- entries,
58
- get: (id) => byId.get(String(id || '').trim()) || null,
59
- get tokens() { return entries.reduce((a, e) => a + e.tokens, 0); },
60
- };
61
- }
62
-
63
- /**
64
- * WHERE IT CAME FROM, not the whole address bar.
65
- *
66
- * A Google results URL carries about a thousand characters of tracking parameters — sca_esv,
67
- * gs_lp, sclient — and the manifest printed every one of them, on every turn, in a line whose
68
- * job is "here is a source you may read". That is a quarter of a thousand tokens of noise the
69
- * model cannot use and must still read past.
70
- *
71
- * The host and path identify the source; the query string is kept only when it is short
72
- * enough to be meaning rather than machinery. An unparseable URL is left exactly as given —
73
- * guessing at a string we could not read is how a source becomes unidentifiable.
74
- */
75
- export function shortUrl(url, max = 120) {
76
- const raw = String(url || '');
77
- if (!raw || raw.length <= max) return raw;
78
- try {
79
- const u = new URL(raw);
80
- const base = `${u.host}${u.pathname}`.replace(/\/$/, '');
81
- // A short query is often the whole point of the address ("?q=how+do+tides+work");
82
- // a long one is machinery. Keep the first parameter when it fits, drop the rest.
83
- const first = [...u.searchParams.entries()][0];
84
- const q = first && `${first[0]}=${first[1]}`;
85
- const withQ = q && `${base}?${q}`;
86
- const out = withQ && withQ.length <= max ? withQ : base;
87
- return out.length <= max ? `${out}…` : `${out.slice(0, max)}…`;
88
- } catch {
89
- return `${raw.slice(0, max)}…`;
90
- }
91
- }
92
-
93
- /** One line per source: what it is, where it came from, what it would cost to read. */
94
- export function manifestText(store) {
95
- if (!store?.entries?.length) return '';
96
- const lines = store.entries.map((e) => `- ${e.id} — ${e.title}${e.url ? ` (${shortUrl(e.url)})` : ''} · ~${e.tokens} tokens`);
97
- return [
98
- 'Attached sources (NOT included below — read them with the `source` tool):',
99
- ...lines,
100
- ].join('\n');
101
- }
102
-
103
- /**
104
- * Score a chunk against the query by term overlap.
105
- *
106
- * Deliberately not a model call and not an embedding: retrieval that needs a model to decide
107
- * what to retrieve costs a round trip before the real one, and this runs on every source read.
108
- * Term overlap is crude and instant, and for "which paragraph of this page mentions X" crude
109
- * is usually right.
110
- */
111
- function scoreChunk(chunk, terms) {
112
- if (!terms.length) return 0;
113
- const hay = chunk.toLowerCase();
114
- let score = 0;
115
- for (const t of terms) {
116
- if (!hay.includes(t)) continue;
117
- // Rarer terms are worth more, approximated by length: 'authentication' discriminates,
118
- // 'the' does not.
119
- score += Math.min(3, t.length / 4);
120
- }
121
- return score;
122
- }
123
-
124
- const STOP = new Set(['the', 'and', 'for', 'this', 'that', 'with', 'from', 'what', 'when', 'where', 'which', 'have', 'has', 'was', 'are', 'you', 'your', 'can', 'about', 'into', 'does', 'did', 'how', 'why', 'all', 'any', 'its']);
125
-
126
- export function queryTerms(query) {
127
- return String(query || '')
128
- .toLowerCase()
129
- .split(/[^a-z0-9]+/)
130
- .filter((t) => t.length > 2 && !STOP.has(t));
131
- }
132
-
133
- /**
134
- * Read a source, optionally narrowed by a query.
135
- *
136
- * Returns `{ id, title, url, text, truncated, of }`. `truncated` is stated rather than
137
- * implied: a model handed a silently-cut document answers confidently about the part it was
138
- * not given, and there is no way for it — or the person reading the answer — to tell.
139
- */
140
- export function readSource(store, { id, query = '', maxTokens = 2000 } = {}) {
141
- const entry = store?.get?.(id);
142
- if (!entry) {
143
- const known = (store?.entries || []).map((e) => e.id).join(', ');
144
- return { error: `No attached source '${id}'.${known ? ` Available: ${known}.` : ' Nothing is attached.'}` };
145
- }
146
- const budget = Math.max(200, Number(maxTokens) || 2000);
147
- if (entry.tokens <= budget) {
148
- return { id: entry.id, title: entry.title, url: entry.url, text: entry.text, truncated: false, of: entry.tokens };
149
- }
150
- const terms = queryTerms(query);
151
- const chunks = entry.text.split(CHUNK_SPLIT).filter((c) => c.trim());
152
- if (!terms.length) {
153
- // No query: the head is the honest default — it is the only part we can justify choosing
154
- // without being told what matters.
155
- const text = entry.text.slice(0, budget * 4);
156
- return { id: entry.id, title: entry.title, url: entry.url, text, truncated: true, of: entry.tokens,
157
- note: `Showing the first ~${approxTokens(text)} of ${entry.tokens} tokens. Pass a query to get the relevant parts instead.` };
158
- }
159
- // Keep the ORIGINAL ORDER of whatever is selected. Ranking by score and presenting in score
160
- // order rearranges a document, and a reordered document reads as a different argument.
161
- const ranked = chunks
162
- .map((c, i) => ({ c, i, s: scoreChunk(c, terms) }))
163
- .filter((x) => x.s > 0)
164
- .sort((a, b) => b.s - a.s);
165
- const picked = [];
166
- let used = 0;
167
- for (const x of ranked) {
168
- const t = approxTokens(x.c);
169
- if (used + t > budget) continue;
170
- picked.push(x);
171
- used += t;
172
- }
173
- if (!picked.length) {
174
- const text = entry.text.slice(0, budget * 4);
175
- return { id: entry.id, title: entry.title, url: entry.url, text, truncated: true, of: entry.tokens,
176
- note: `Nothing in this source matched '${query}'. Showing the beginning instead.` };
177
- }
178
- picked.sort((a, b) => a.i - b.i);
179
- const gaps = picked.some((x, n) => n > 0 && x.i !== picked[n - 1].i + 1);
180
- return {
181
- id: entry.id,
182
- title: entry.title,
183
- url: entry.url,
184
- text: picked.map((x) => x.c).join(gaps ? '\n\n[…]\n\n' : '\n\n'),
185
- truncated: true,
186
- of: entry.tokens,
187
- note: `${picked.length} of ${chunks.length} sections, matched on '${query}'.`,
188
- };
189
- }
8
+ `),truncated:!0,of:t.tokens,note:`${l.length} of ${a.length} sections, matched on '${o}'.`}}export{d as approxTokens,S as makeSourceStore,b as manifestText,m as queryTerms,y as readSource,k as shortUrl,p as sourceId};
package/sources.js CHANGED
@@ -1,273 +1 @@
1
- /**
2
- * WHERE THE CONTENT CAME FROM DECIDES HOW FAR IT MAY TRAVEL.
3
- *
4
- * A model was summarising an internal access-management page and the turn went to a public
5
- * inference host, because routing asked what the WORK needed — tools, vision, quality — and
6
- * never asked where the material came from. Capability decided; provenance did not exist.
7
- *
8
- * This module supplies the missing question. It classifies a source URL as internal or not,
9
- * and turns that into a REACH CEILING the router already knows how to enforce. Two properties
10
- * make it a guard rather than a preference:
11
- *
12
- * - It only ever NARROWS. `meetReach` takes the tighter of two ceilings, so no later step,
13
- * plugin or user dial can widen what an internal source already restricted. A guard that
14
- * something downstream can relax is a suggestion.
15
- * - It fails CLOSED. An unparseable URL is treated as internal, because the alternative is
16
- * to send data outward on the strength of a string we could not read.
17
- *
18
- * Pure and dependency-free: the same rules must hold in the extension, the gateway and the
19
- * bridge, or "internal" means three different things and the strictest one is not the one
20
- * that runs.
21
- */
22
-
23
- const REACH_ORDER = ['device', 'trusted', 'any'];
24
-
25
- /** The tighter of two reach ceilings. Guards compose by MEET — they can only narrow. */
26
- export function meetReach(a, b) {
27
- const ia = REACH_ORDER.indexOf(a);
28
- const ib = REACH_ORDER.indexOf(b);
29
- // An unknown value is not a licence to travel further: treat it as the tightest.
30
- if (ia < 0) return REACH_ORDER.includes(b) ? b : 'device';
31
- if (ib < 0) return a;
32
- return REACH_ORDER[Math.min(ia, ib)];
33
- }
34
-
35
- /**
36
- * Hosts that are internal by NETWORK TOPOLOGY — the STARTING list, not a floor.
37
- *
38
- * Deliberately limited to what the address itself proves. A corporate wiki on a public SaaS
39
- * domain looks exactly like any other public host from here — that case needs the user's own
40
- * patterns, and pretending to detect it would give a false sense of coverage.
41
- *
42
- * These are seeded into the user's editable list rather than silently prepended to it,
43
- * because "internal" is a fact about someone's network, not about ours: a developer testing
44
- * against localhost may well want that traffic to reach a cloud model, and a rule they cannot
45
- * see is a rule they cannot correct.
46
- */
47
- export const INTERNAL_PATTERN_CATALOG = Object.freeze([
48
- { pattern: 'localhost', label: 'This machine, by name' },
49
- { pattern: '127.0.0.0/8', label: 'Loopback' },
50
- { pattern: '::1', label: 'Loopback (IPv6)' },
51
- { pattern: '0.0.0.0/8', label: 'This network' },
52
- { pattern: '10.0.0.0/8', label: 'Private network' },
53
- { pattern: '172.16.0.0/12', label: 'Private network' },
54
- { pattern: '192.168.0.0/16', label: 'Private network (home / office)' },
55
- { pattern: '100.64.0.0/10', label: 'Carrier-grade NAT — used by some corporate networks' },
56
- { pattern: '169.254.0.0/16', label: 'Link-local — never routes off this segment' },
57
- { pattern: 'fe80::/10', label: 'Link-local (IPv6)' },
58
- { pattern: 'fc00::/7', label: 'Unique local (IPv6) — the 10.x of IPv6' },
59
- { pattern: '*.internal', label: 'Reserved name' },
60
- { pattern: '*.intranet', label: 'Reserved name' },
61
- { pattern: '*.corp', label: 'Conventional corporate suffix' },
62
- { pattern: '*.lan', label: 'Conventional LAN suffix' },
63
- { pattern: '*.local', label: 'mDNS / Bonjour' },
64
- { pattern: '*.localdomain', label: 'Default suffix on many routers' },
65
- { pattern: '*.home', label: 'Home network' },
66
- { pattern: '*.home.arpa', label: 'Home network (RFC 8375)' },
67
- { pattern: '*.private', label: 'Conventional private suffix' },
68
- { pattern: '*.test', label: 'Reserved for testing (RFC 2606)' },
69
- { pattern: '*.invalid', label: 'Reserved as never-resolvable' },
70
- { pattern: '<intranet>', label: 'Any bare hostname with no dots, e.g. http://wiki/ — includes localhost' },
71
- ]);
72
-
73
- /**
74
- * The patterns on by default. Derived from the catalog so the list a UI offers and the list
75
- * the classifier applies cannot drift — two copies of this would mean a rule someone can see
76
- * and not switch off, or switch off and not escape.
77
- */
78
- export const DEFAULT_INTERNAL_PATTERNS = Object.freeze(INTERNAL_PATTERN_CATALOG.map((x) => x.pattern));
79
- const IPV4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
80
-
81
- function ipToInt(host) {
82
- const m = IPV4.exec(host);
83
- if (!m) return null;
84
- const parts = m.slice(1).map(Number);
85
- if (parts.some((n) => n > 255)) return null;
86
- return parts.reduce((acc, n) => acc * 256 + n, 0);
87
- }
88
-
89
- /**
90
- * An IPv6 address as its 128 bits, or null if it is not one. Bits rather than a normalised
91
- * string because prefix matching (fc00::/7, fe80::/10) is a bit-length comparison — a
92
- * textual "starts with" would get /7 wrong, since the boundary falls inside a hex digit.
93
- */
94
- function v6Bits(host) {
95
- const h = String(host || '').replace(/^\[|\]$/g, '').toLowerCase();
96
- if (!h.includes(':')) return null;
97
- const [headRaw, tailRaw, ...rest] = h.split('::');
98
- if (rest.length) return null; // more than one '::' is not a valid address
99
- const parse = (part) => (part ? part.split(':').filter((x) => x !== '') : []);
100
- let head = parse(headRaw);
101
- let tail = tailRaw === undefined ? [] : parse(tailRaw);
102
- // A trailing IPv4 form (::ffff:10.0.0.1) — the last group is four octets, not two.
103
- const last = (tail.length ? tail : head)[Math.max(0, (tail.length ? tail : head).length - 1)];
104
- if (last && last.includes('.')) {
105
- const v4 = ipToInt(last);
106
- if (v4 == null) return null;
107
- const pair = [(v4 >>> 16).toString(16), (v4 & 0xffff).toString(16)];
108
- if (tail.length) tail = [...tail.slice(0, -1), ...pair];
109
- else head = [...head.slice(0, -1), ...pair];
110
- }
111
- const missing = 8 - head.length - tail.length;
112
- if (tailRaw === undefined ? missing !== 0 : missing < 0) return null;
113
- const groups = [...head, ...Array(Math.max(0, missing)).fill('0'), ...tail];
114
- if (groups.length !== 8) return null;
115
- let bits = '';
116
- for (const g of groups) {
117
- if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
118
- bits += parseInt(g, 16).toString(2).padStart(16, '0');
119
- }
120
- return bits;
121
- }
122
-
123
- function inCidr(host, cidr) {
124
- const [net, bitsRaw] = cidr.split('/');
125
- const hostV6 = v6Bits(host);
126
- const netV6 = v6Bits(net);
127
- if (hostV6 || netV6) {
128
- // A v4 host never sits inside a v6 range, and vice versa — comparing them would be a
129
- // type confusion that quietly matches or quietly does not.
130
- if (!hostV6 || !netV6) return false;
131
- const n = Number(bitsRaw);
132
- if (!Number.isFinite(n) || n < 0 || n > 128) return false;
133
- return hostV6.slice(0, n) === netV6.slice(0, n);
134
- }
135
- const ip = ipToInt(host);
136
- const base = ipToInt(net);
137
- if (ip == null || base == null) return false;
138
- const bits = Number(bitsRaw);
139
- if (!Number.isFinite(bits) || bits < 0 || bits > 32) return false;
140
- // Shifting by 32 is a no-op in JS, so /0 is spelled out rather than computed.
141
- const mask = bits === 0 ? 0 : (-1 << (32 - bits)) >>> 0;
142
- return (ip & mask) >>> 0 === (base & mask) >>> 0;
143
- }
144
-
145
- /**
146
- * Does `host` match one pattern? Supported forms, in the order a person would expect:
147
- * `*.example.com` the domain and every subdomain
148
- * `example.com` the same — a bare domain covers its subdomains, because someone adding
149
- * their company domain means the whole company, not one host
150
- * `10.0.0.0/8` a CIDR range
151
- * `<intranet>` any single-label host, which can only resolve on a private network
152
- */
153
- export function hostMatches(host, pattern) {
154
- const h = String(host || '').toLowerCase().replace(/\.$/, '');
155
- const p = String(pattern || '').toLowerCase().trim();
156
- if (!h || !p) return false;
157
- // A NAME with no dots — not an address. A public IPv6 literal has no dots either, and
158
- // sweeping it in here would have quietly pinned every v6 host on the internet as internal.
159
- if (p === '<intranet>') return !h.includes('.') && !h.includes(':') && !h.startsWith('[') && !IPV4.test(h);
160
- if (p.includes('/')) return inCidr(h, p);
161
- // '::1' and '[::1]' and '0:0:0:0:0:0:0:1' are one address written three ways.
162
- const pv6 = v6Bits(p);
163
- if (pv6) return v6Bits(h) === pv6;
164
- const bare = p.startsWith('*.') ? p.slice(2) : p;
165
- if (h === bare) return true;
166
- if (h.endsWith(`.${bare}`)) return true;
167
- // A leading wildcard anywhere else ("*.corp.*") is not supported rather than
168
- // half-supported: a pattern that silently matches nothing is worse than one that is
169
- // rejected, because it looks like protection.
170
- return false;
171
- }
172
-
173
- /**
174
- * Classify one source. Returns `{ internal, host, matched }`.
175
- *
176
- * FAILS CLOSED. A URL we cannot parse counts as internal: we would rather keep a public page
177
- * on-device than send an internal one out because a string was malformed. The same applies to
178
- * non-http schemes — a `file:` path is local by definition.
179
- */
180
- export function classifySource(url, { patterns = DEFAULT_INTERNAL_PATTERNS } = {}) {
181
- const raw = String(url || '').trim();
182
- if (!raw) return { internal: false, host: '', matched: null };
183
- let host = '';
184
- let scheme = '';
185
- try {
186
- const u = new URL(raw);
187
- host = u.hostname;
188
- scheme = u.protocol.replace(':', '');
189
- } catch {
190
- return { internal: true, host: '', matched: 'unparseable' };
191
- }
192
- if (scheme === 'file') return { internal: true, host, matched: 'file:' };
193
- // Extension and browser-internal pages carry no third-party content, and are not sources
194
- // anyone means to protect — treating them as internal would pin every turn to a local
195
- // model for no reason.
196
- if (/^(chrome|edge|about|moz|chrome-extension|data|blob)$/.test(scheme)) {
197
- return { internal: false, host, matched: null };
198
- }
199
- for (const p of patterns) {
200
- if (hostMatches(host, p)) return { internal: true, host, matched: p };
201
- }
202
- return { internal: false, host, matched: null };
203
- }
204
-
205
- /**
206
- * Every url written in a piece of text.
207
- *
208
- * Declared sources are not the only way internal material enters a turn: someone pastes a
209
- * link to an internal runbook, or a tool result comes back carrying one. The address is the
210
- * evidence, and it is evidence wherever it appears — so the same classifier that reads the
211
- * tab reads the body too.
212
- *
213
- * Explicit schemes only. A bare host like `wiki/page` is indistinguishable from an ordinary
214
- * path, and matching it would pin turns on text that mentions no site at all — a guard that
215
- * fires on prose gets switched off, which protects nobody.
216
- */
217
- export function extractUrls(text) {
218
- const out = [];
219
- const re = /\b(?:https?|file):\/\/[^\s<>"'`)\]}]+/gi;
220
- for (const m of String(text || '').matchAll(re)) {
221
- // Trailing punctuation belongs to the sentence, not to the address.
222
- out.push(m[0].replace(/[.,;:!?]+$/, ''));
223
- }
224
- return out;
225
- }
226
-
227
- /**
228
- * Build the reach policy for a turn from everything it draws on.
229
- *
230
- * ANY internal source pins the WHOLE turn. A turn that mixes an internal page with a public
231
- * one is still carrying the internal page, and splitting the difference would send it out.
232
- *
233
- * `ceiling` is what an internal source narrows to — 'device' for local models only, or
234
- * 'trusted' to also allow a workspace gateway the user runs. It cannot widen anything: the
235
- * result is always the tighter of the ceiling and what was already required.
236
- */
237
- export function sourcePolicyFor(sources = [], { patterns, ceiling = 'device', base = 'any' } = {}) {
238
- const list = (Array.isArray(sources) ? sources : [sources]).filter(Boolean);
239
- const hits = [];
240
- for (const s of list) {
241
- const url = typeof s === 'string' ? s : (s?.url || s?.href || '');
242
- if (!url) continue;
243
- const c = classifySource(url, patterns ? { patterns } : undefined);
244
- if (c.internal) hits.push({ url, host: c.host, matched: c.matched });
245
- }
246
- if (!hits.length) return { internal: false, reach: base, hits: [], why: null };
247
- const safeCeiling = REACH_ORDER.includes(ceiling) ? ceiling : 'device';
248
- return {
249
- internal: true,
250
- reach: meetReach(base, safeCeiling),
251
- hits,
252
- // Named so the person can see WHICH rule pinned the turn — an unexplained restriction
253
- // gets switched off wholesale.
254
- why: `${hits[0].host || 'the source'} matches '${hits[0].matched}' — kept ${safeCeiling === 'device' ? 'on this device' : 'inside your workspace'}`,
255
- };
256
- }
257
-
258
- /**
259
- * Every address a conversation carries: each attachment's `url`, every URL in a message
260
- * body (an internal link pasted into a message — or arriving in a tool result written back
261
- * into the conversation — is internal material just as much as an attachment is), plus
262
- * anything the caller states outright. The WHOLE conversation, because an internal page
263
- * attached three turns ago is still in the text being sent now.
264
- */
265
- export function sourceUrlsOf(messages, extraSources = []) {
266
- const urls = [];
267
- for (const m of messages || []) {
268
- for (const a of m?.attachments || []) if (a?.url) urls.push(a.url);
269
- for (const u of extractUrls(typeof m?.content === 'string' ? m.content : '')) urls.push(u);
270
- }
271
- for (const s of extraSources || []) if (s) urls.push(typeof s === 'string' ? s : (s.url || s.href || ''));
272
- return urls.filter(Boolean);
273
- }
1
+ const h=["device","trusted","any"];function v(n,r){const t=h.indexOf(n),e=h.indexOf(r);return t<0?h.includes(r)?r:"device":e<0?n:h[Math.min(t,e)]}const x=Object.freeze([{pattern:"localhost",label:"This machine, by name"},{pattern:"127.0.0.0/8",label:"Loopback"},{pattern:"::1",label:"Loopback (IPv6)"},{pattern:"0.0.0.0/8",label:"This network"},{pattern:"10.0.0.0/8",label:"Private network"},{pattern:"172.16.0.0/12",label:"Private network"},{pattern:"192.168.0.0/16",label:"Private network (home / office)"},{pattern:"100.64.0.0/10",label:"Carrier-grade NAT — used by some corporate networks"},{pattern:"169.254.0.0/16",label:"Link-local — never routes off this segment"},{pattern:"fe80::/10",label:"Link-local (IPv6)"},{pattern:"fc00::/7",label:"Unique local (IPv6) — the 10.x of IPv6"},{pattern:"*.internal",label:"Reserved name"},{pattern:"*.intranet",label:"Reserved name"},{pattern:"*.corp",label:"Conventional corporate suffix"},{pattern:"*.lan",label:"Conventional LAN suffix"},{pattern:"*.local",label:"mDNS / Bonjour"},{pattern:"*.localdomain",label:"Default suffix on many routers"},{pattern:"*.home",label:"Home network"},{pattern:"*.home.arpa",label:"Home network (RFC 8375)"},{pattern:"*.private",label:"Conventional private suffix"},{pattern:"*.test",label:"Reserved for testing (RFC 2606)"},{pattern:"*.invalid",label:"Reserved as never-resolvable"},{pattern:"<intranet>",label:"Any bare hostname with no dots, e.g. http://wiki/ — includes localhost"}]),w=Object.freeze(x.map(n=>n.pattern)),g=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;function m(n){const r=g.exec(n);if(!r)return null;const t=r.slice(1).map(Number);return t.some(e=>e>255)?null:t.reduce((e,l)=>e*256+l,0)}function d(n){const r=String(n||"").replace(/^\[|\]$/g,"").toLowerCase();if(!r.includes(":"))return null;const[t,e,...l]=r.split("::");if(l.length)return null;const o=u=>u?u.split(":").filter(p=>p!==""):[];let s=o(t),a=e===void 0?[]:o(e);const i=(a.length?a:s)[Math.max(0,(a.length?a:s).length-1)];if(i&&i.includes(".")){const u=m(i);if(u==null)return null;const p=[(u>>>16).toString(16),(u&65535).toString(16)];a.length?a=[...a.slice(0,-1),...p]:s=[...s.slice(0,-1),...p]}const c=8-s.length-a.length;if(e===void 0?c!==0:c<0)return null;const f=[...s,...Array(Math.max(0,c)).fill("0"),...a];if(f.length!==8)return null;let b="";for(const u of f){if(!/^[0-9a-f]{1,4}$/.test(u))return null;b+=parseInt(u,16).toString(2).padStart(16,"0")}return b}function y(n,r){const[t,e]=r.split("/"),l=d(n),o=d(t);if(l||o){if(!l||!o)return!1;const f=Number(e);return!Number.isFinite(f)||f<0||f>128?!1:l.slice(0,f)===o.slice(0,f)}const s=m(n),a=m(t);if(s==null||a==null)return!1;const i=Number(e);if(!Number.isFinite(i)||i<0||i>32)return!1;const c=i===0?0:-1<<32-i>>>0;return(s&c)>>>0===(a&c)>>>0}function R(n,r){const t=String(n||"").toLowerCase().replace(/\.$/,""),e=String(r||"").toLowerCase().trim();if(!t||!e)return!1;if(e==="<intranet>")return!t.includes(".")&&!t.includes(":")&&!t.startsWith("[")&&!g.test(t);if(e.includes("/"))return y(t,e);const l=d(e);if(l)return d(t)===l;const o=e.startsWith("*.")?e.slice(2):e;return!!(t===o||t.endsWith(`.${o}`))}function k(n,{patterns:r=w}={}){const t=String(n||"").trim();if(!t)return{internal:!1,host:"",matched:null};let e="",l="";try{const o=new URL(t);e=o.hostname,l=o.protocol.replace(":","")}catch{return{internal:!0,host:"",matched:"unparseable"}}if(l==="file")return{internal:!0,host:e,matched:"file:"};if(/^(chrome|edge|about|moz|chrome-extension|data|blob)$/.test(l))return{internal:!1,host:e,matched:null};for(const o of r)if(R(e,o))return{internal:!0,host:e,matched:o};return{internal:!1,host:e,matched:null}}function A(n){const r=[],t=/\b(?:https?|file):\/\/[^\s<>"'`)\]}]+/gi;for(const e of String(n||"").matchAll(t))r.push(e[0].replace(/[.,;:!?]+$/,""));return r}function N(n=[],{patterns:r,ceiling:t="device",base:e="any"}={}){const l=(Array.isArray(n)?n:[n]).filter(Boolean),o=[];for(const a of l){const i=typeof a=="string"?a:a?.url||a?.href||"";if(!i)continue;const c=k(i,r?{patterns:r}:void 0);c.internal&&o.push({url:i,host:c.host,matched:c.matched})}if(!o.length)return{internal:!1,reach:e,hits:[],why:null};const s=h.includes(t)?t:"device";return{internal:!0,reach:v(e,s),hits:o,why:`${o[0].host||"the source"} matches '${o[0].matched}' — kept ${s==="device"?"on this device":"inside your workspace"}`}}function C(n,r=[]){const t=[];for(const e of n||[]){for(const l of e?.attachments||[])l?.url&&t.push(l.url);for(const l of A(typeof e?.content=="string"?e.content:""))t.push(l)}for(const e of r||[])e&&t.push(typeof e=="string"?e:e.url||e.href||"");return t.filter(Boolean)}export{w as DEFAULT_INTERNAL_PATTERNS,x as INTERNAL_PATTERN_CATALOG,k as classifySource,A as extractUrls,R as hostMatches,v as meetReach,N as sourcePolicyFor,C as sourceUrlsOf};