@chatpanel/events 0.356.1 → 0.358.2

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/access-request.js CHANGED
@@ -1 +1 @@
1
- import{EventError as W}from"./event.js";import{SECRET_PATHS as z}from"./sandbox-policy.js";import{egressAllowed as N}from"./policy.js";const p=e=>!!e&&typeof e=="object"&&!Array.isArray(e),d=(e,n)=>{if(!e)throw new W("SHAPE",n)},O=e=>[...new Set(e)],f=Object.freeze(["path","host"]),v=Object.freeze(["once","session","always"]),x=Object.freeze(["read","write"]),E=Object.freeze(["refused","ahead"]),H=Object.freeze(["grant","deny","ask"]),R=500,u=e=>{let n=String(e||"").trim();for(;n.length>1&&(n.endsWith("/")||n.endsWith("\\"));)n=n.slice(0,-1);return n},w=(e,n)=>n&&(e==="~"||e.startsWith("~/")||e.startsWith("~\\"))?n+e.slice(1):e;function g(e,n){const t=u(e),s=u(n);return!t||!s?!1:t===s||t.startsWith(`${s}/`)||t.startsWith(`${s}\\`)}function C(e,n=""){const t=u(w(u(e),n));return z.some(s=>g(t,u(s))||g(t,u(w(s,n))))}function P(e,n=""){const t=u(w(u(e),n));if(!t||t==="/"||t==="~"||/^[a-z]:$/i.test(t)||/^[a-z]:\\?$/i.test(t)||n&&t===u(n))return!0;const s=t.split(/[/\\]/).filter(Boolean).length;return t.startsWith("/")&&s<2}function b(e){d(p(e),"access request: must be an object");const n=e.what;d(p(n),"access request: what { kind, value, mode } required");const t=String(n.kind||"");d(f.includes(t),`access request: kind must be one of ${f.join(", ")}`);const s=t==="path"?u(n.value):String(n.value||"").trim().toLowerCase();d(s.length>0&&s.length<1024&&!s.includes("\0"),`access request: not a ${t}: ${JSON.stringify(n.value)}`);const i=n.mode==null?"write":String(n.mode);d(x.includes(i),`access request: mode must be one of ${x.join(", ")}`);const a=e.scope==null?"session":String(e.scope);d(v.includes(a),`access request: scope must be one of ${v.join(", ")}`);const o=e.asked==null?"refused":String(e.asked);return d(E.includes(o),`access request: asked must be one of ${E.join(", ")}`),{id:String(e.id||""),at:Number(e.at)||0,commandId:String(e.commandId||""),engine:String(e.engine||""),agentId:String(e.agentId||""),runId:String(e.runId||""),what:{kind:t,value:s,mode:i},why:String(e.why||"").slice(0,R),scope:a,asked:o,state:"pending"}}function D(e,{home:n=""}={}){d(p(e),"delegation: must be an object");const t=e.what;d(p(t),"delegation: what { kind, under | host, mode } required");const s=String(t.kind||"");d(f.includes(s),`delegation: kind must be one of ${f.join(", ")}`);const i=t.mode==null?"write":String(t.mode);d(x.includes(i),`delegation: mode must be one of ${x.join(", ")}`);const a=e.scope==null?"session":String(e.scope);d(a==="once"||a==="session","delegation: scope must be once or session — a standing grant is a person's decision, not a rule's");const o={id:String(e.id||""),label:String(e.label||"").slice(0,120),enabled:e.enabled!==!1,what:{kind:s,mode:i},scope:a};if(s==="path"){const r=u(t.under);d(r.length>0,"delegation: what.under is required — a rule without a root is not a bounded rule"),d(!C(r,n),`delegation: ${r} is a secret path and can never be granted`),d(!P(r,n),`delegation: ${r} is too broad to delegate — name the project folder, not the disk`),o.what.under=r}else{const r=String(t.host||t.under||"").trim().toLowerCase();d(/^(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i.test(r)&&r!=="*",`delegation: not a host pattern: ${JSON.stringify(t.host)}`),o.what.host=r}return o.ttlMs=Math.max(0,Math.min(Number(e.ttlMs)||0,365*24*3600*1e3)),o.maxPerRun=e.maxPerRun==null?0:Math.max(0,Math.min(Number(e.maxPerRun)||0,1e3)),o.agents=O((Array.isArray(e.agents)?e.agents:[]).map(r=>String(r).trim()).filter(Boolean)),o.expiresAt=Number(e.expiresAt)||0,o.createdBy=String(e.createdBy||""),o}function L(e,n,{home:t=""}={}){if(!e?.enabled||e.what.kind!==n.what.kind||e.what.mode==="read"&&n.what.mode==="write"||e.agents.length&&!e.agents.includes(n.engine)&&!e.agents.includes(n.agentId))return!1;if(e.what.kind==="path")return g(w(n.what.value,t),w(e.what.under,t));const s=n.what.value;return e.what.host.startsWith("*.")?s===e.what.host.slice(2)||s.endsWith(e.what.host.slice(1)):s===e.what.host}const k=Object.freeze({ALLOW:"allow",ALWAYS:"allow_always",DENY:"deny"});function M(e){return e===k.ALWAYS?"always":"session"}function I(e,{home:n=""}={}){d(p(e),"access grant: must be an object");const t=e.what;d(p(t),"access grant: what required");const s=String(t.kind||"");d(f.includes(s),`access grant: kind must be one of ${f.join(", ")}`);const i=s==="path"?u(t.value):String(t.value||"").trim().toLowerCase();d(i.length>0,"access grant: what.value required"),d(!(s==="path"&&C(i,n)),`access grant: ${i} is a secret path and can never be granted`);const a=String(e.scope||"session");return d(v.includes(a),`access grant: scope must be one of ${v.join(", ")}`),{id:String(e.id||""),what:{kind:s,value:i,mode:t.mode==="read"?"read":"write"},scope:a,allow:e.allow!==!1,at:Number(e.at)||0,expiresAt:Number(e.expiresAt)||0,by:String(e.by||""),label:String(e.label||"").slice(0,200)}}function q(e,n,t,{now:s=0,newId:i=()=>"",home:a=""}={}){const o=b(n);if(t===k.DENY)return[...y(e,a),I({id:i(),what:o.what,scope:"session",allow:!1,at:s,by:"person"},{home:a})];const r=M(t),c=I({id:i(),what:o.what,scope:r,allow:!0,at:s,by:"person",label:o.what.value},{home:a}),S=[];for(const h of y(e,a)){if(h.allow&&j(h,o,{home:a,now:s})&&$(h.scope)>=$(r))return y(e,a);h.allow===c.allow&&h.what.kind===c.what.kind&&$(h.scope)<=$(r)&&j(c,{what:h.what,engine:o.engine},{home:a})||S.push(h)}return[...S,c]}const $=e=>e==="always"?2:e==="session"?1:0,y=(e,n="")=>(Array.isArray(e)?e:[]).map(t=>{try{return I(t,{home:n})}catch{return null}}).filter(Boolean);function j(e,n,{home:t="",now:s=0}={}){if(!e||e.expiresAt&&s&&s>e.expiresAt||e.what.kind!==n.what.kind||e.allow&&e.what.mode==="read"&&n.what.mode==="write")return!1;if(e.what.kind==="path")return g(w(n.what.value,t),w(e.what.value,t));const i=n.what.value;return e.what.value.startsWith("*.")?i===e.what.value.slice(2)||i.endsWith(e.what.value.slice(1)):i===e.what.value}function _(e,n,{home:t="",now:s=0}={}){const i=b(n),a=y(e,t).filter(r=>j(r,i,{home:t,now:s}));if(!a.length)return null;const o=a.find(r=>r.allow);return o?{verdict:"grant",by:{kind:"person",id:o.by},ruleId:o.id,reason:o.scope==="always"?"you allowed this folder":"you allowed this earlier in this chat",scope:o.scope,expiresAt:o.expiresAt||0}:{verdict:"deny",by:{kind:"person",id:a[0].by},ruleId:a[0].id,reason:"you denied this earlier in this chat",scope:"session"}}function F(e,n,{home:t=""}={}){return y(e,t).filter(s=>s.id!==String(n))}function G(e,{now:n=0,home:t=""}={}){return y(e,t).filter(s=>s.allow&&s.scope==="always"&&!(s.expiresAt&&n&&n>s.expiresAt))}const m=(e,n,t={})=>({verdict:e,by:n,ruleId:"",reason:"",...t});function J({request:e,delegations:n=[],policy:t=null,now:s=0,usedPerRule:i={},home:a="",canAsk:o=!0,grants:r=[]}={}){const c=b(e);if(c.what.kind==="path"&&C(c.what.value,a))return m("deny",{kind:"floor"},{reason:`${c.what.value} holds credentials or ChatPanel's own store, and is never grantable`});if(t&&c.what.kind==="host"&&!N(t,c.what.value))return m("deny",{kind:"policy"},{reason:`your organisation does not allow ${c.what.value}`});if(t&&c.what.kind==="path"&&Array.isArray(t.access?.deny)){const A=t.access.deny.find(l=>g(w(c.what.value,a),w(u(l),a)));if(A)return m("deny",{kind:"policy"},{reason:`your organisation does not allow access under ${A}`})}const S=_(r,c,{home:a,now:s});if(S)return S;const h=t?.access?.delegation!=="never";if(h)for(const A of n){let l;try{l=D(A,{home:a})}catch{continue}if(L(l,c,{home:a})&&!(l.expiresAt&&s&&s>l.expiresAt)&&!(l.maxPerRun&&(i[l.id]||0)>=l.maxPerRun))return m("grant",{kind:"delegation",id:l.createdBy||""},{ruleId:l.id,reason:l.label?`allowed by your rule “${l.label}”`:"allowed by a rule you set",scope:l.scope,expiresAt:l.ttlMs&&s?s+l.ttlMs:0})}return o?m("ask",{kind:"person"},{scope:c.scope}):m("deny",{kind:"floor"},{reason:h?"nothing here can approve this, and an unanswered request is a denial":"your organisation requires a person to approve this, and this run has nobody to ask"})}function K(e={},{newId:n=()=>"",now:t=0}={}){const s=e.path?"path":"host";return b({id:e.id||n()||"",at:e.at||t||0,commandId:e.commandId||"",engine:e.engine||"",agentId:e.agentId||"",runId:e.runId||"",what:{kind:s,value:s==="path"?e.path:e.host,mode:e.mode||"write"},why:e.why||"",scope:"session",asked:"refused"})}function V(e,{home:n=""}={}){const t=b(e),s=t.engine||t.agentId||"This agent",i=c=>n&&g(c,u(n))?`~${c.slice(u(n).length)}`:c,a=t.what.kind==="path",o=a?i(t.what.value):t.what.value,r=t.what.mode==="read"?"read":a?"write in":"reach";return{title:`${s} wants to ${r} ${a&&o.split(/[/\\]/).filter(Boolean).pop()||o}`,detail:o,kind:t.what.kind,why:t.why,answers:[{id:k.ALLOW,label:"Allow",hint:"for the rest of this chat"},{id:k.ALWAYS,label:`Always allow ${o}`,hint:"revoke any time in Settings › Runtime"},{id:k.DENY,label:"Deny",hint:""}]}}function X(e,n){const t=b(e),s=(t.what.kind==="path",t.what.value),i=t.engine||t.agentId||"the agent",a=t.what.mode==="read"?"read":"write in";if(n?.verdict==="grant"){const o=n.by?.kind==="delegation"?n.reason||"allowed by a rule you set":"allowed by you";return`${i} asked to ${a} ${s} — ${o}`}return n?.verdict==="deny"?`${i} asked to ${a} ${s} — denied${n.reason?` (${n.reason})`:""}`:`${i} is asking to ${a} ${s}`}export{k as ACCESS_ANSWERS,E as ACCESS_ASKED,f as ACCESS_KINDS,x as ACCESS_MODES,v as ACCESS_SCOPES,H as ACCESS_VERDICTS,R as WHY_MAX,K as accessRequestFromRefusal,J as decideAccess,L as delegationCovers,V as describeAccessCard,X as describeDecision,j as grantCovers,C as isSecretPath,P as isTooBroadRoot,I as normalizeAccessGrant,b as normalizeAccessRequest,D as normalizeDelegation,g as pathInside,_ as recallAccess,q as rememberAccess,F as revokeAccess,M as scopeForAnswer,G as standingGrants};
1
+ import{EventError as j}from"./event.js";import{SECRET_PATHS as z}from"./sandbox-policy.js";import{egressAllowed as N}from"./policy.js";const p=e=>!!e&&typeof e=="object"&&!Array.isArray(e),c=(e,n)=>{if(!e)throw new j("SHAPE",n)},R=e=>[...new Set(e)],f=Object.freeze(["path","host"]),v=Object.freeze(["once","session","always"]),x=Object.freeze(["read","write"]),I=Object.freeze(["refused","ahead"]),H=Object.freeze(["grant","deny","ask"]),O=500,u=e=>{let n=String(e||"").trim();for(;n.length>1&&(n.endsWith("/")||n.endsWith("\\"));)n=n.slice(0,-1);return n},w=(e,n)=>n&&(e==="~"||e.startsWith("~/")||e.startsWith("~\\"))?n+e.slice(1):e;function g(e,n){const t=u(e),s=u(n);return!t||!s?!1:t===s||t.startsWith(`${s}/`)||t.startsWith(`${s}\\`)}function E(e,n=""){const t=u(w(u(e),n));return z.some(s=>g(t,u(s))||g(t,u(w(s,n))))}function M(e,n=""){const t=u(w(u(e),n));if(!t||t==="/"||t==="~"||/^[a-z]:$/i.test(t)||/^[a-z]:\\?$/i.test(t)||n&&t===u(n))return!0;const s=t.split(/[/\\]/).filter(Boolean).length;return t.startsWith("/")&&s<2}function b(e){c(p(e),"access request: must be an object");const n=e.what;c(p(n),"access request: what { kind, value, mode } required");const t=String(n.kind||"");c(f.includes(t),`access request: kind must be one of ${f.join(", ")}`);const s=t==="path"?u(n.value):String(n.value||"").trim().toLowerCase();c(s.length>0&&s.length<1024&&!s.includes("\0"),`access request: not a ${t}: ${JSON.stringify(n.value)}`);const o=n.mode==null?"write":String(n.mode);c(x.includes(o),`access request: mode must be one of ${x.join(", ")}`);const a=e.scope==null?"session":String(e.scope);c(v.includes(a),`access request: scope must be one of ${v.join(", ")}`);const i=e.asked==null?"refused":String(e.asked);return c(I.includes(i),`access request: asked must be one of ${I.join(", ")}`),{id:String(e.id||""),at:Number(e.at)||0,commandId:String(e.commandId||""),engine:String(e.engine||""),agentId:String(e.agentId||""),runId:String(e.runId||""),what:{kind:t,value:s,mode:o},why:String(e.why||"").slice(0,O),scope:a,asked:i,state:"pending"}}function P(e,{home:n=""}={}){c(p(e),"delegation: must be an object");const t=e.what;c(p(t),"delegation: what { kind, under | host, mode } required");const s=String(t.kind||"");c(f.includes(s),`delegation: kind must be one of ${f.join(", ")}`);const o=t.mode==null?"write":String(t.mode);c(x.includes(o),`delegation: mode must be one of ${x.join(", ")}`);const a=e.scope==null?"session":String(e.scope);c(a==="once"||a==="session","delegation: scope must be once or session — a standing grant is a person's decision, not a rule's");const i={id:String(e.id||""),label:String(e.label||"").slice(0,120),enabled:e.enabled!==!1,what:{kind:s,mode:o},scope:a};if(s==="path"){const r=u(t.under);c(r.length>0,"delegation: what.under is required — a rule without a root is not a bounded rule"),c(!E(r,n),`delegation: ${r} is a secret path and can never be granted`),c(!M(r,n),`delegation: ${r} is too broad to delegate — name the project folder, not the disk`),i.what.under=r}else{const r=String(t.host||t.under||"").trim().toLowerCase();c(/^(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i.test(r)&&r!=="*",`delegation: not a host pattern: ${JSON.stringify(t.host)}`),i.what.host=r}return i.ttlMs=Math.max(0,Math.min(Number(e.ttlMs)||0,365*24*3600*1e3)),i.maxPerRun=e.maxPerRun==null?0:Math.max(0,Math.min(Number(e.maxPerRun)||0,1e3)),i.agents=R((Array.isArray(e.agents)?e.agents:[]).map(r=>String(r).trim()).filter(Boolean)),i.expiresAt=Number(e.expiresAt)||0,i.createdBy=String(e.createdBy||""),i}function _(e,n,{home:t=""}={}){if(!e?.enabled||e.what.kind!==n.what.kind||e.what.mode==="read"&&n.what.mode==="write"||e.agents.length&&!e.agents.includes(n.engine)&&!e.agents.includes(n.agentId))return!1;if(e.what.kind==="path")return g(w(n.what.value,t),w(e.what.under,t));const s=n.what.value;return e.what.host.startsWith("*.")?s===e.what.host.slice(2)||s.endsWith(e.what.host.slice(1)):s===e.what.host}const k=Object.freeze({ALLOW:"allow",ALWAYS:"allow_always",DENY:"deny"});function D(e){return e===k.ALWAYS?"always":"session"}function C(e,{home:n=""}={}){c(p(e),"access grant: must be an object");const t=e.what;c(p(t),"access grant: what required");const s=String(t.kind||"");c(f.includes(s),`access grant: kind must be one of ${f.join(", ")}`);const o=s==="path"?u(t.value):String(t.value||"").trim().toLowerCase();c(o.length>0,"access grant: what.value required"),c(!(s==="path"&&E(o,n)),`access grant: ${o} is a secret path and can never be granted`);const a=String(e.scope||"session");return c(v.includes(a),`access grant: scope must be one of ${v.join(", ")}`),{id:String(e.id||""),what:{kind:s,value:o,mode:t.mode==="read"?"read":"write"},scope:a,allow:e.allow!==!1,at:Number(e.at)||0,expiresAt:Number(e.expiresAt)||0,by:String(e.by||""),label:String(e.label||"").slice(0,200)}}function F(e,n,t,{now:s=0,newId:o=()=>"",home:a=""}={}){const i=b(n);if(t===k.DENY)return[...y(e,a),C({id:o(),what:i.what,scope:"session",allow:!1,at:s,by:"person"},{home:a})];const r=D(t),d=C({id:o(),what:i.what,scope:r,allow:!0,at:s,by:"person",label:i.what.value},{home:a}),m=[];for(const h of y(e,a)){if(h.allow&&W(h,i,{home:a,now:s})&&$(h.scope)>=$(r))return y(e,a);h.allow===d.allow&&h.what.kind===d.what.kind&&$(h.scope)<=$(r)&&W(d,{what:h.what,engine:i.engine},{home:a})||m.push(h)}return[...m,d]}const $=e=>e==="always"?2:e==="session"?1:0,y=(e,n="")=>(Array.isArray(e)?e:[]).map(t=>{try{return C(t,{home:n})}catch{return null}}).filter(Boolean);function W(e,n,{home:t="",now:s=0}={}){if(!e||e.expiresAt&&s&&s>e.expiresAt||e.what.kind!==n.what.kind||e.allow&&e.what.mode==="read"&&n.what.mode==="write")return!1;if(e.what.kind==="path")return g(w(n.what.value,t),w(e.what.value,t));const o=n.what.value;return e.what.value.startsWith("*.")?o===e.what.value.slice(2)||o.endsWith(e.what.value.slice(1)):o===e.what.value}function L(e,n,{home:t="",now:s=0}={}){const o=b(n),a=y(e,t).filter(r=>W(r,o,{home:t,now:s}));if(!a.length)return null;const i=a.find(r=>r.allow);return i?{verdict:"grant",by:{kind:"person",id:i.by},ruleId:i.id,reason:i.scope==="always"?"you allowed this folder":"you allowed this earlier in this chat",scope:i.scope,expiresAt:i.expiresAt||0}:{verdict:"deny",by:{kind:"person",id:a[0].by},ruleId:a[0].id,reason:"you denied this earlier in this chat",scope:"session"}}function G(e,n,{home:t=""}={}){return y(e,t).filter(s=>s.id!==String(n))}function J(e,{now:n=0,home:t=""}={}){return y(e,t).filter(s=>s.allow&&s.scope==="always"&&!(s.expiresAt&&n&&n>s.expiresAt))}const S=(e,n,t={})=>({verdict:e,by:n,ruleId:"",reason:"",...t});function K({request:e,delegations:n=[],policy:t=null,now:s=0,usedPerRule:o={},home:a="",canAsk:i=!0,grants:r=[]}={}){const d=b(e);if(d.what.kind==="path"&&E(d.what.value,a))return S("deny",{kind:"floor"},{reason:`${d.what.value} holds credentials or ChatPanel's own store, and is never grantable`});if(t&&d.what.kind==="host"&&!N(t,d.what.value))return S("deny",{kind:"policy"},{reason:`your organisation does not allow ${d.what.value}`});if(t&&d.what.kind==="path"&&Array.isArray(t.access?.deny)){const A=t.access.deny.find(l=>g(w(d.what.value,a),w(u(l),a)));if(A)return S("deny",{kind:"policy"},{reason:`your organisation does not allow access under ${A}`})}const m=L(r,d,{home:a,now:s});if(m)return m;const h=t?.access?.delegation!=="never";if(h)for(const A of n){let l;try{l=P(A,{home:a})}catch{continue}if(_(l,d,{home:a})&&!(l.expiresAt&&s&&s>l.expiresAt)&&!(l.maxPerRun&&(o[l.id]||0)>=l.maxPerRun))return S("grant",{kind:"delegation",id:l.createdBy||""},{ruleId:l.id,reason:l.label?`allowed by your rule “${l.label}”`:"allowed by a rule you set",scope:l.scope,expiresAt:l.ttlMs&&s?s+l.ttlMs:0})}return i?S("ask",{kind:"person"},{scope:d.scope}):S("deny",{kind:"floor"},{reason:h?"nothing here can approve this, and an unanswered request is a denial":"your organisation requires a person to approve this, and this run has nobody to ask"})}function V(e={},{newId:n=()=>"",now:t=0}={}){const s=e.path?"path":"host";return b({id:e.id||n()||"",at:e.at||t||0,commandId:e.commandId||"",engine:e.engine||"",agentId:e.agentId||"",runId:e.runId||"",what:{kind:s,value:s==="path"?e.path:e.host,mode:e.mode||"write"},why:e.why||"",scope:"session",asked:"refused"})}function X(e={}){const n=p(e.input)?e.input:{},t=String(e.tool||"");let o=((/^(Write|Edit|MultiEdit|NotebookEdit)$/.test(t)?String(n.file_path||n.notebook_path||""):"")||String(e.path||"")).trim();if(!o)for(const a of Array.isArray(e.rules)?e.rules:[]){if(!/^(Read|Edit|Write|MultiEdit|NotebookEdit)$/.test(String(a?.tool||"")))continue;const i=String(a.content||"").trim().replace(/\/\*\*$|\/\*$/,"");if(i.startsWith("//")){o=i.slice(1);break}if(i==="~"||i.startsWith("~/")){o=i;break}}return o&&!o.includes("\0")?o:null}function Q(e,{home:n=""}={}){const t=b(e),s=t.engine||t.agentId||"This agent",o=d=>n&&g(d,u(n))?`~${d.slice(u(n).length)}`:d,a=t.what.kind==="path",i=a?o(t.what.value):t.what.value,r=t.what.mode==="read"?"read":a?"write in":"reach";return{title:`${s} wants to ${r} ${a&&i.split(/[/\\]/).filter(Boolean).pop()||i}`,detail:i,kind:t.what.kind,why:t.why,answers:[{id:k.ALLOW,label:"Allow",hint:"for the rest of this chat"},{id:k.ALWAYS,label:`Always allow ${i}`,hint:"revoke any time in Settings › Runtime"},{id:k.DENY,label:"Deny",hint:""}]}}function U(e,n){const t=b(e),s=(t.what.kind==="path",t.what.value),o=t.engine||t.agentId||"the agent",a=t.what.mode==="read"?"read":"write in";if(n?.verdict==="grant"){const i=n.by?.kind==="delegation"?n.reason||"allowed by a rule you set":"allowed by you";return`${o} asked to ${a} ${s} — ${i}`}return n?.verdict==="deny"?`${o} asked to ${a} ${s} — denied${n.reason?` (${n.reason})`:""}`:`${o} is asking to ${a} ${s}`}export{k as ACCESS_ANSWERS,I as ACCESS_ASKED,f as ACCESS_KINDS,x as ACCESS_MODES,v as ACCESS_SCOPES,H as ACCESS_VERDICTS,O as WHY_MAX,V as accessRequestFromRefusal,K as decideAccess,_ as delegationCovers,Q as describeAccessCard,U as describeDecision,W as grantCovers,E as isSecretPath,M as isTooBroadRoot,C as normalizeAccessGrant,b as normalizeAccessRequest,P as normalizeDelegation,g as pathInside,X as permissionAccessTarget,L as recallAccess,F as rememberAccess,G as revokeAccess,D as scopeForAnswer,J as standingGrants};
@@ -1,2 +1,2 @@
1
- const $="permission_request",p=Object.freeze({ALLOW:"allow",ALLOW_CHAT:"allow_chat",DENY:"deny"}),W=600*1e3,_="permission_prompt";function N(){return{name:_,description:"Chap-internal: the person answers the agent's permission prompt. Not for the model.",parameters:{type:"object",properties:{tool:{type:"string"},input:{type:"object"},description:{type:"string"},path:{type:"string"},rules:{type:"array"}}}}}const T=t=>t===_,u=t=>t!==null&&typeof t=="object"&&!Array.isArray(t),a=t=>typeof t=="string"?t:t==null?"":String(t);function k(t={}){const e=u(t)?t:{},o=a(e.tool_name||e.toolName||e.tool),n=[];for(const s of Array.isArray(e.permission_suggestions)?e.permission_suggestions:[])if(!(!u(s)||s.type!=="addRules"||s.behavior==="deny"))for(const r of Array.isArray(s.rules)?s.rules:[]){if(!u(r))continue;const c=a(r.toolName||o),i=a(r.ruleContent);c&&!n.some(f=>f.tool===c&&f.content===i)&&n.push({tool:c,content:i})}return{tool:o,displayName:a(e.display_name||e.displayName)||o,input:u(e.input)?e.input:{},description:a(e.description),path:a(e.blocked_path||e.path)||null,rules:n,toolUseId:a(e.tool_use_id||e.toolUseId)||null}}function g(t={}){const e=Array.isArray(t.rules)?t.rules.filter(o=>o&&o.tool):[];return e.length?e.map(o=>o.content?`${o.tool}(${o.content})`:o.tool):t.tool?[String(t.tool)]:[]}function L(t={}){const e=a(t.tool),o=u(t.input)?t.input:{},n=(l,d=240)=>l.length>d?`${l.slice(0,d-1)}…`:l;let s,r;if(e==="Bash")s="Run a command",r=n(a(o.command).trim()||a(t.description)||"a shell command");else if(e==="Edit"||e==="MultiEdit"||e==="NotebookEdit")s="Edit a file",r=a(o.file_path||o.notebook_path||t.path)||"a file";else if(e==="Write"){s="Write a file";const l=a(o.file_path||t.path)||"a file",d=a(o.content).length;r=d?`${l} (${d.toLocaleString()} chars)`:l}else if(/^mcp__/.test(e))s="Use a connected server",r=e.replace(/^mcp__/,"").replace(/__/," › ");else{s=`Use ${t.displayName||e||"a tool"}`;let l="";try{l=JSON.stringify(o)}catch{l=""}r=a(t.description)||(l&&l!=="{}"?n(l,200):e)}const c=g(t),i=Array.isArray(t.rules)?t.rules.filter(l=>l?.content):[],f=i.length&&i.some(l=>l.content!==r)?i.map(l=>l.content).join(", "):c.length&&!i.length?`every ${e} call`:null;return{title:s,detail:r,scope:f,description:a(t.description)||null}}function h(t,e=""){return t===p.ALLOW?{behavior:"allow",scope:"once"}:t===p.ALLOW_CHAT?{behavior:"allow",scope:"chat"}:{behavior:"deny",scope:"once",...e?{message:String(e)}:{}}}function w(t){return u(t)&&t.behavior==="allow"}function E(t,e){const o=new Set(Array.isArray(t)?t:[]);for(const n of g(e))o.add(n);return[...o].sort()}const b="*";function O(t,e){const o=new Set(Array.isArray(t)?t:[]);if(o.has(b))return h(p.ALLOW);const n=g(e);return n.length&&n.every(s=>o.has(s))?h(p.ALLOW):null}function R(t,e,o={}){const n=u(o)?o:{},s=w(e);let r;if(s){if(r={behavior:"allow",updatedInput:u(n.input)?n.input:{}},e.scope==="chat"){const c=(Array.isArray(n.permission_suggestions)?n.permission_suggestions:[]).filter(i=>u(i)&&i.type==="addRules"&&i.behavior!=="deny"&&Array.isArray(i.rules)&&i.rules.length).map(i=>({type:"addRules",rules:i.rules,behavior:"allow",destination:"session"}));c.length&&(r.updatedPermissions=c)}}else r={behavior:"deny",message:a(e?.message)||"The user declined this action in Chap. Continue without it, or explain what it was for and ask."};return{type:"control_response",response:{subtype:"success",request_id:String(t),response:r}}}function C(t,e){const{detail:o}=L(t),n=w(e),s=n&&e.scope==="chat"?" for this chat":"";return`${n?"Allowed":"Declined"}${s}: ${o}`}function x(t){return t==="Bash"?"code":/^(Edit|MultiEdit|NotebookEdit)$/.test(String(t))?"pencil":t==="Write"?"file":/^mcp__/.test(String(t))?"plug":"bot"}function P({memory:t=new Map}={}){const e=t;async function o(n,{convId:s=null,confirm:r,agentName:c="The agent"}={}){const i=O(await e.get(s)||[],n);if(i)return i;const{title:f,detail:l,scope:d,description:y}=L(n),S=[l,y&&y!==l?y:"",d?`“Allow for this chat” also covers: ${d}`:""].filter(Boolean).join(`
2
- `);let m;try{if(typeof r!="function")throw new Error("no card");m=await r(S,{title:`${c} asks — ${f.toLowerCase()}?`,iconName:x(n.tool),scopeLabel:"Allow for this chat",allLabel:"Allow all in this chat"})}catch(A){return h(p.DENY,`Chap could not show the permission card (${A?.message||A}).`)}return m==="all"?(await e.set(s,[b]),h(p.ALLOW_CHAT)):m==="site"||m==="always"?(await e.set(s,E(await e.get(s)||[],n)),h(p.ALLOW_CHAT)):h(m==="allow"?p.ALLOW:p.DENY)}return{ask:o,forget(n){e.delete(n)},standing(n){return e.get(n)||[]},provider({convId:n=null,confirm:s,agentName:r="The agent"}={}){return{id:"permission",specs:[N()],execute:async(c,i)=>JSON.stringify(await o(i&&typeof i=="object"?i:{},{convId:n,confirm:s,agentName:r}))}}}}export{b as ALLOW_ALL,p as PERMISSION_ANSWERS,$ as PERMISSION_EVENT,W as PERMISSION_TIMEOUT_MS,_ as PERMISSION_TOOL_NAME,R as claudePermissionResponse,P as createPermissionDesk,L as describePermission,T as isPermissionTool,w as permissionAllowed,h as permissionAnswer,x as permissionIcon,g as permissionKeys,C as permissionLine,k as permissionRequestFromClaude,N as permissionToolSpec,O as recallPermission,E as rememberPermission};
1
+ const T="permission_request",u=Object.freeze({ALLOW:"allow",ALLOW_CHAT:"allow_chat",DENY:"deny"}),W=600*1e3,w="permission_prompt";function E(){return{name:w,description:"Chap-internal: the person answers the agent's permission prompt. Not for the model.",parameters:{type:"object",properties:{tool:{type:"string"},input:{type:"object"},description:{type:"string"},path:{type:"string"},rules:{type:"array"}}}}}const k=t=>t===w,p=t=>t!==null&&typeof t=="object"&&!Array.isArray(t),a=t=>typeof t=="string"?t:t==null?"":String(t);function R(t={}){const e=p(t)?t:{},o=a(e.tool_name||e.toolName||e.tool),s=[];for(const n of Array.isArray(e.permission_suggestions)?e.permission_suggestions:[])if(!(!p(n)||n.type!=="addRules"||n.behavior==="deny"))for(const l of Array.isArray(n.rules)?n.rules:[]){if(!p(l))continue;const c=a(l.toolName||o),i=a(l.ruleContent);c&&!s.some(f=>f.tool===c&&f.content===i)&&s.push({tool:c,content:i})}return{tool:o,displayName:a(e.display_name||e.displayName)||o,input:p(e.input)?e.input:{},description:a(e.description),path:a(e.blocked_path||e.path)||null,rules:s,toolUseId:a(e.tool_use_id||e.toolUseId)||null}}function A(t={}){const e=Array.isArray(t.rules)?t.rules.filter(o=>o&&o.tool):[];return e.length?e.map(o=>o.content?`${o.tool}(${o.content})`:o.tool):t.tool?[String(t.tool)]:[]}function L(t={}){const e=a(t.tool),o=p(t.input)?t.input:{},s=(r,d=240)=>r.length>d?`${r.slice(0,d-1)}…`:r;let n,l;if(e==="Bash")n="Run a command",l=s(a(o.command).trim()||a(t.description)||"a shell command");else if(e==="Edit"||e==="MultiEdit"||e==="NotebookEdit")n="Edit a file",l=a(o.file_path||o.notebook_path||t.path)||"a file";else if(e==="Write"){n="Write a file";const r=a(o.file_path||t.path)||"a file",d=a(o.content).length;l=d?`${r} (${d.toLocaleString()} chars)`:r}else if(/^mcp__/.test(e))n="Use a connected server",l=e.replace(/^mcp__/,"").replace(/__/," › ");else{n=`Use ${t.displayName||e||"a tool"}`;let r="";try{r=JSON.stringify(o)}catch{r=""}l=a(t.description)||(r&&r!=="{}"?s(r,200):e)}const c=A(t),i=Array.isArray(t.rules)?t.rules.filter(r=>r?.content):[],f=i.length&&i.some(r=>r.content!==l)?i.map(r=>r.content).join(", "):c.length&&!i.length?`every ${e} call`:null,h=p(t.access)?a(t.access.folder):"",g=h?`This works in ${h}, outside the folders it may change. Allowing it opens ${h} to it for the rest of this chat.`:null;return{title:n,detail:l,scope:f,description:a(t.description)||null,access:g}}function m(t,e=""){return t===u.ALLOW?{behavior:"allow",scope:"once"}:t===u.ALLOW_CHAT?{behavior:"allow",scope:"chat"}:{behavior:"deny",scope:"once",...e?{message:String(e)}:{}}}function b(t){return p(t)&&t.behavior==="allow"}function O(t,e){const o=new Set(Array.isArray(t)?t:[]);for(const s of A(e))o.add(s);return[...o].sort()}const S="*";function x(t,e){const o=new Set(Array.isArray(t)?t:[]);if(o.has(S))return m(u.ALLOW);const s=A(e);return s.length&&s.every(n=>o.has(n))?m(u.ALLOW):null}function C(t,e,o={}){const s=p(o)?o:{},n=b(e);let l;if(n){if(l={behavior:"allow",updatedInput:p(s.input)?s.input:{}},e.scope==="chat"){const c=(Array.isArray(s.permission_suggestions)?s.permission_suggestions:[]).filter(i=>p(i)&&i.type==="addRules"&&i.behavior!=="deny"&&Array.isArray(i.rules)&&i.rules.length).map(i=>({type:"addRules",rules:i.rules,behavior:"allow",destination:"session"}));c.length&&(l.updatedPermissions=c)}}else l={behavior:"deny",message:a(e?.message)||"The user declined this action in Chap. Continue without it, or explain what it was for and ask."};return{type:"control_response",response:{subtype:"success",request_id:String(t),response:l}}}function P(t,e){const{detail:o}=L(t),s=b(e),n=s&&e.scope==="chat"?" for this chat":"";return`${s?"Allowed":"Declined"}${n}: ${o}`}function $(t){return t==="Bash"?"code":/^(Edit|MultiEdit|NotebookEdit)$/.test(String(t))?"pencil":t==="Write"?"file":/^mcp__/.test(String(t))?"plug":"bot"}function I({memory:t=new Map}={}){const e=t;async function o(s,{convId:n=null,confirm:l,agentName:c="The agent"}={}){const i=p(s.access)?null:x(await e.get(n)||[],s);if(i)return i;const{title:f,detail:h,scope:g,description:r,access:d}=L(s),N=[d,h,r&&r!==h?r:"",g?`“Allow for this chat” also covers: ${g}`:""].filter(Boolean).join(`
2
+ `);let y;try{if(typeof l!="function")throw new Error("no card");y=await l(N,{title:`${c} asks — ${f.toLowerCase()}?`,iconName:$(s.tool),scopeLabel:"Allow for this chat",allLabel:"Allow all in this chat"})}catch(_){return m(u.DENY,`Chap could not show the permission card (${_?.message||_}).`)}return y==="all"?(await e.set(n,[S]),m(u.ALLOW_CHAT)):y==="site"||y==="always"?(await e.set(n,O(await e.get(n)||[],s)),m(u.ALLOW_CHAT)):m(y==="allow"?u.ALLOW:u.DENY)}return{ask:o,forget(s){e.delete(s)},standing(s){return e.get(s)||[]},provider({convId:s=null,confirm:n,agentName:l="The agent"}={}){return{id:"permission",specs:[E()],execute:async(c,i)=>JSON.stringify(await o(i&&typeof i=="object"?i:{},{convId:s,confirm:n,agentName:l}))}}}}export{S as ALLOW_ALL,u as PERMISSION_ANSWERS,T as PERMISSION_EVENT,W as PERMISSION_TIMEOUT_MS,w as PERMISSION_TOOL_NAME,C as claudePermissionResponse,I as createPermissionDesk,L as describePermission,k as isPermissionTool,b as permissionAllowed,m as permissionAnswer,$ as permissionIcon,A as permissionKeys,P as permissionLine,R as permissionRequestFromClaude,E as permissionToolSpec,x as recallPermission,O as rememberPermission};
package/link-fetch.js ADDED
@@ -0,0 +1 @@
1
+ import{createLinkEndpoint as M}from"./link-connect.js";import{noiseCrypto as Q}from"./link-noise.js";import{parsePairingCode as X,admissionToken as Y,toB64url as _,fromB64url as D,linkPathAllowed as Z}from"./link-protocol.js";import{relaySocketUrl as ee,tunnelSocketUrl as te}from"./link-routes.js";const I=1,re="chatpanel-link.v1",i=(e,s)=>Object.assign(new TypeError(`link: ${e}`),{code:s}),L=()=>{try{return new DOMException("The operation was aborted.","AbortError")}catch{return Object.assign(new Error("aborted"),{name:"AbortError"})}},U=e=>{try{return new URL(e).host}catch{return"the relay"}};function ne(e){if(!(e&&typeof e=="object"&&e.v===I&&typeof e.room=="string"&&typeof e.token=="string"&&typeof e.deviceKey=="string"&&typeof e.gatewayKey=="string"&&(typeof e.relay=="string"||typeof e.gw=="string")))throw i("the stored device state is not a ChatPanel Link device (pair again with a new code)","bad_state");return e}function oe(e){const s=e.gw&&te(e.gw,e.room),f=e.relay&&ee(e.relay,e.room);return e.route==="tailscale"||e.route==="cloudflare"?[s].filter(Boolean):e.route==="link"||e.route==="relay"?[f].filter(Boolean):[f,s].filter(Boolean)}const V=oe;function ae(e){return e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(0)}function ie(e){const s={};if(!e)return s;if(typeof e.forEach=="function"&&!Array.isArray(e))return e.forEach((f,g)=>{s[g]=f}),s;for(const[f,g]of Array.isArray(e)?e:Object.entries(e))g!=null&&(s[f]=String(g));return s}async function se(e){return e==null?null:typeof e=="string"||e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):typeof URLSearchParams<"u"&&e instanceof URLSearchParams?e.toString():new Uint8Array(await new Response(e).arrayBuffer())}async function de({pairing:e=null,state:s=null,store:f=null,name:g="Partner server",WebSocketImpl:T=globalThis.WebSocket,crypto:P=Q(),now:$=()=>Date.now(),connectTimeoutMs:F=15e3,keepaliveMs:x=25e3,setTimer:O=setTimeout,clearTimer:R=clearTimeout}={}){if(typeof T!="function")throw i("no WebSocket here — pass WebSocketImpl (the `ws` package on Node 20)","no_websocket");let b=s||(f?await f.load():null)||null;function C(n,t,{keyPair:r,psk:o=null,peerKey:c=null,laptopKey:k=null,onClose:d=()=>{}}){return new Promise((h,K)=>{let y=null,m=!1;const v=O(()=>l(i(`the gateway did not answer through ${U(n)} — is it running and online?`,"unreachable")),F);function l(a,p){if(!m)if(m=!0,R(v),a){try{y?.close(1e3,"done")}catch{}K(a)}else h(p)}const u=M({side:"phone",keyPair:r,psk:o,peerKey:c,laptopKey:k,name:String(g||"").slice(0,60),crypto:P,sendBytes:a=>{try{y.send(a)}catch{}},onSession:a=>l(null,{ws:y,endpoint:u,session:a}),onEnd:a=>{/^handshake/.test(String(a))&&l(i(`the gateway refused this device (${String(a).replace(/^handshake: /,"")})`,"refused"))}});try{y=new T(n,[re,t])}catch(a){l(i(`cannot open ${U(n)}: ${a?.message||a}`,"unreachable"));return}y.binaryType="arraybuffer",y.onmessage=a=>typeof a.data=="string"?u.onText(a.data):u.onBinary(ae(a.data)),y.onerror=()=>{},y.onclose=a=>{u.close("socket closed");const p=a?.code;d(p),l(p===4003||p===4004?i("the computer’s owner removed this device — pair again with a new code","revoked"):p===1008?i(`${U(n)} did not admit this device — the code expired or was used, or the device was removed`,"refused"):i(`${U(n)} closed the connection (${p||"no code"}) — is the gateway online?`,"unreachable"))}})}async function N(n){const t=X(n,{now:$()}),r=await P.generateKeyPair(),o=_(t.room),c=await Y(t.secret,t.room,"phone");let k=null;for(const d of V({relay:t.relay,gw:t.gw,room:o,route:t.route}))try{const h=await C(d,c,{keyPair:r,psk:t.secret,laptopKey:t.key});h.session.close("paired");try{h.ws.close(1e3,"paired")}catch{}return{v:I,route:t.route,relay:t.relay,gw:t.gw,room:o,token:c,deviceKey:_(r.raw),gatewayKey:_(t.key),pairedAt:$()}}catch(h){if(k=h,h?.code==="refused")break}throw k||i("the code names no way to reach the gateway","bad_code")}if(!b){if(!e)throw i("pass `pairing` (a cplink1. code from the computer’s owner) or a `store` holding an earlier pairing","not_paired");b=await N(e),f&&await f.save(b)}ne(b);const A=b;let j=null,w=null,E=null,q=!1;const W=n=>!!n&&!n.dead&&!n.session.closed;async function G(){j||=await P.keyPairFromPrivate(D(A.deviceKey));let n=null;for(const t of V(A)){const r={dead:!1};try{Object.assign(r,await C(t,A.token,{keyPair:j,peerKey:D(A.gatewayKey),onClose:c=>{r.dead=!0,R(r.keepalive),(c===4003||c===4004)&&(E=i("the computer’s owner removed this device — pair again with a new code","revoked"))}}));const o=()=>{if(!r.dead){try{r.ws.send("ping")}catch{}r.keepalive=O(o,x),r.keepalive?.unref?.()}};return r.keepalive=O(o,x),r.keepalive?.unref?.(),r}catch(o){if(n=o,o?.code==="revoked")throw E=o,o;if(o?.code==="refused")throw o}}throw n}async function H(n){if(q)throw i("this Link fetch was closed","closed");if(E)throw E;if(w){const r=await w.catch(()=>null);if(W(r))return r;w=null}const t=G();if(w=t,t.catch(()=>{w===t&&(w=null)}),!n)return t;if(n.aborted)throw L();return Promise.race([t,new Promise((r,o)=>n.addEventListener("abort",()=>o(L()),{once:!0}))])}async function z(n,t={}){const r=typeof Request<"u"&&n instanceof Request?n:null,o=new URL(r?r.url:String(n),"http://127.0.0.1"),c=String(t.method||r?.method||"GET").toUpperCase(),k=ie(t.headers??r?.headers),d=t.signal??r?.signal??null;if(d?.aborted)throw L();const h=o.pathname+o.search;if(!Z(h))throw i(`only the gateway's /v1/ API crosses Link (asked for ${o.pathname})`,"path");const K=t.body!==void 0?t.body:r&&c!=="GET"&&c!=="HEAD"?await r.arrayBuffer():null,y=await se(K),m=await H(d),v=new AbortController,l=()=>v.abort();d?.addEventListener?.("abort",l,{once:!0});let u;try{u=await m.session.request({method:c,path:h,headers:k,body:y,signal:v.signal})}catch(S){throw d?.removeEventListener?.("abort",l),d?.aborted?L():(m.session.closed&&(m.dead=!0),i(S?.message?.replace(/^link: /,"")||"the request failed","request"))}const a=u.body[Symbol.asyncIterator](),J=u.status===204||u.status===205||u.status===304||c==="HEAD"?null:new ReadableStream({async pull(S){try{const B=await a.next();B.done?(d?.removeEventListener?.("abort",l),S.close()):S.enqueue(B.value)}catch(B){d?.removeEventListener?.("abort",l),S.error(v.signal.aborted?L():i(B?.message?.replace(/^link: /,"")||"the response was cut off","stream"))}},cancel(){v.abort()}});return new Response(J,{status:u.status,headers:u.headers})}return Object.defineProperties(z,{deviceId:{value:A.room,enumerable:!0},revoked:{get:()=>E,enumerable:!0},close:{value:()=>{q=!0;const n=w;w=null,n?.then(t=>{t.dead=!0,R(t.keepalive),t.endpoint.close("closed");try{t.ws.close(1e3,"closed")}catch{}},()=>{})}}})}export{I as LINK_DEVICE_STATE_VERSION,ne as checkLinkDeviceState,de as createLinkFetch,oe as linkSocketUrls};
package/link-protocol.js CHANGED
@@ -1 +1 @@
1
- import{concatBytes as J}from"./link-noise.js";const C=1,U="chatpanel-link",P=6e4,G=44e3,W=Object.freeze(["GET","POST","PUT","PATCH","DELETE"]),Y=64,we=Object.freeze(["batch"]),re=2*P,V=Object.freeze({bytesPerSecond:9e5,byteBurst:6e6,framesPerSecond:90,frameBurst:800});function ne({bytesPerSecond:e,byteBurst:r,framesPerSecond:n,frameBurst:l}=V,{now:c=()=>Date.now(),sleep:u=a=>new Promise(s=>setTimeout(s,a))}={}){let a=r,s=l,d=c();return async E=>{for(;;){const p=c(),m=Math.max(0,p-d)/1e3;if(d=p,a=Math.min(r,a+m*e),s=Math.min(l,s+m*n),a>=E&&s>=1){a-=E,s-=1;return}const f=Math.max((E-a)/e,(1-s)/n)*1e3;await u(Math.max(1,Math.ceil(f)))}}}const _=new TextEncoder,I=new TextDecoder,q=Object.freeze({firstMs:0,windowMs:100,maxBytes:16e3});async function*fe(e,{firstMs:r=q.firstMs,windowMs:n=q.windowMs,maxBytes:l=q.maxBytes,setTimer:c=setTimeout,clearTimer:u=clearTimeout}={}){const a=e[Symbol.asyncIterator]();let s=[],d=0,E=!0,p=null;const m=()=>{const f=s.length===1?s[0]:J(...s);return s=[],d=0,E=!1,f};try{for(;;){if(p||=a.next(),!d){const h=await p;if(p=null,h.done)return;if(s.push(h.value),d+=h.value.length,E&&r<=0){yield m();continue}}let f=null;const g=new Promise(h=>{f=c(()=>h(null),E?r:n)});try{for(;d<l;){p||=a.next();const h=await Promise.race([p,g]);if(h===null)break;if(p=null,h.done){yield m();return}s.push(h.value),d+=h.value.length}}finally{u(f)}yield m()}}finally{p&&p.catch(()=>{});try{a.return?.()?.catch?.(()=>{})}catch{}}}function S(e){let r="";for(const n of e)r+=String.fromCharCode(n);return btoa(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function oe(e){const r=String(e||"").replace(/-/g,"+").replace(/_/g,"/");if(!/^[A-Za-z0-9+/]*$/.test(r))throw new Error("link: not base64url");const n=atob(r+"=".repeat((4-r.length%4)%4));return Uint8Array.from(n,l=>l.charCodeAt(0))}const ie=e=>{let r="";for(const n of e)r+=String.fromCharCode(n);return btoa(r)},ae=e=>Uint8Array.from(atob(String(e||"")),r=>r.charCodeAt(0));function pe({relay:e=null,gw:r=null,room:n,key:l,secret:c,name:u=""}){const a=X({v:String(C),relay:e,gw:r,room:S(n),key:S(l),psk:S(c),name:u}),s=new URLSearchParams({v:String(C)});return a.relay&&s.set("relay",a.relay),s.set("room",S(a.room)),s.set("key",S(a.key)),s.set("psk",S(a.secret)),a.gw&&s.set("gw",a.gw),a.name&&s.set("name",a.name),`${U}:?${s.toString()}`}function ge(e){const r=String(e||"").trim();if(!r.startsWith(`${U}:?`))throw new Error("link: not a ChatPanel pairing code");const n=new URLSearchParams(r.slice(U.length+2));return X(Object.fromEntries(n.entries()))}function X(e){if(String(e.v)!==String(C))throw new Error(`link: pairing code version ${e.v} is not supported — update the app`);const r=(a,s)=>{let d;try{d=new URL(String(a))}catch{throw new Error(`link: the pairing code's ${s} address is not readable`)}if(d.protocol!=="https:"||d.username||d.password)throw new Error(`link: the ${s} must be an https address`);return d.origin},n=e.relay?r(e.relay,"relay"):null,l=e.gw?r(e.gw,"tunnel"):null;if(!n&&!l)throw new Error("link: the pairing code has no relay address");const c=(a,s)=>{let d;try{d=oe(e[a])}catch{throw new Error(`link: ${a} is not readable`)}if(d.length!==s)throw new Error(`link: ${a} must be ${s} bytes`);return d},u=String(e.name||"").replace(/[\u0000-\u001f\u007f<>]/g,"").trim().slice(0,64);return{relay:n,gw:l,room:c("room",16),key:c("key",32),secret:c("psk",32),name:u}}async function me(e,r,n,l=globalThis.crypto.subtle){if(n!=="laptop"&&n!=="phone")throw new Error("link: side is laptop or phone");const c=await l.importKey("raw",e,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),u=await l.sign("HMAC",c,_.encode(`chatpanel-link admit v${C}|${S(r)}|${n}`));return S(new Uint8Array(u))}async function ye(e,r=globalThis.crypto.subtle){const n=new Uint8Array(await r.digest("SHA-256",_.encode(String(e))));return Array.from(n,l=>l.toString(16).padStart(2,"0")).join("")}const se=/^(authorization|cookie|set-cookie|www-authenticate|host|proxy-.*|x-forwarded-.*|forwarded|x-real-ip|cf-.*|tailscale-.*|x-chatpanel-device|x-chatpanel-token|content-length|connection|transfer-encoding|upgrade)$/i;function H(e={}){const r={};for(const[n,l]of Object.entries(e||{})){const c=String(n).toLowerCase();!/^[a-z0-9-]{1,64}$/.test(c)||se.test(c)||(r[c]=String(l).replace(/[\r\n]+/g," ").slice(0,2048))}return r}function Q(e){const r=String(e||"");if(!r.startsWith("/v1/")||r.length>2048)return!1;const n=r.split("?")[0];return!/(^|\/)\.\.?(\/|$)|%2e|%2f|%5c|\\|\/\//i.test(n)}const le=new Set(["req","body","end","res","cancel","error","ping","pong"]);function ce(e){const r=_.encode(JSON.stringify(e));if(r.length>P)throw new Error("link: frame too large");return r}function ue(e){if(!e.length)throw new Error("link: an empty batch");const r=e.reduce((c,u)=>c+u.length+1,1);if(r>P)throw new Error("link: batch too large");const n=new Uint8Array(r);n[0]=91;let l=1;return e.forEach((c,u)=>{n.set(c,l),l+=c.length,n[l++]=u===e.length-1?93:44}),n}function be(e){if(e.length>P)throw new Error("link: frame too large");let r;try{r=JSON.parse(I.decode(e))}catch{throw new Error("link: frame is not JSON")}return z(r)}function de(e,{batch:r=!1}={}){if(e.length>P)throw new Error("link: frame too large");let n;try{n=JSON.parse(I.decode(e))}catch{throw new Error("link: frame is not JSON")}if(!Array.isArray(n))return[z(n)];if(!r)throw new Error("link: a batch this session did not agree to");if(!n.length)throw new Error("link: an empty batch");return n.map(z)}function z(e){if(!e||typeof e!="object"||Array.isArray(e)||!le.has(e.t))throw new Error("link: unknown frame");if(e.t!=="ping"&&e.t!=="pong"&&!(Number.isInteger(e.id)&&e.id>0))throw new Error("link: frame without an id");if(e.t==="req"){if(!W.includes(e.method))throw new Error("link: method not allowed");if(!Q(e.path))throw new Error("link: path not allowed");e.headers=H(e.headers)}if(e.t==="res"&&!(Number.isInteger(e.status)&&e.status>=100&&e.status<600))throw new Error("link: bad status");if(e.t==="body"&&typeof e.data!="string")throw new Error("link: body without data");return e}function Z(){const e=[];let r=null,n=!1,l=null;const c=()=>{const u=r;r=null,u?.()};return{push(u){e.push(u),c()},end(){n=!0,c()},fail(u){l=u,n=!0,c()},async*[Symbol.asyncIterator](){for(;;){if(e.length){yield e.shift();continue}if(l)throw l;if(n)return;await new Promise(u=>{r=u})}},async bytes(){const u=[];for await(const a of this)u.push(a);return J(...u)},async text(){return I.decode(await this.bytes())}}}function Ee({transport:e,send:r,side:n,onRequest:l=null,onClose:c=()=>{},pace:u=V,now:a,sleep:s,batch:d=!1,defer:E=p=>setTimeout(p,0)}={}){if(!e||typeof r!="function")throw new Error("link: transport and send are required");if(n!=="phone"&&n!=="laptop")throw new Error("link: side is phone or laptop");let p=n==="phone"?1:2;const m=t=>t%2===(n==="phone"?1:0);let f=null;const g=new Map,h=new Map;let M=Promise.resolve();const B=u?ne(u,{...a?{now:a}:{},...s?{sleep:s}:{}}):null,K=t=>{const o=M.then(async()=>{if(f)return;const i=await e.encrypt(t);B&&await B(i.length),f||r(i)});return M=o.catch(()=>{}),o};let k=[],T=1,O=null,N=0;const D=()=>{if(O=null,!k.length||f){k=[],T=1;return}const t=k.length===1?k[0]:ue(k);k=[],T=1,N+=t.length,K(t).finally(()=>{N-=t.length}).catch(()=>{})},y=t=>{if(f)return Promise.reject(new Error(`link: session closed (${f})`));const o=ce(t);return d?(T+o.length+1>P&&D(),k.push(o),T+=o.length+1,O||(O=E(D)||!0),N>re?M:Promise.resolve()):K(o)},j=t=>{t?.catch?.(()=>{})};let R=Promise.resolve();const F=async(t,o)=>{for(let i=0;i<o.length;i+=G)await y({t:"body",id:t,data:ie(o.subarray(i,i+G))})};function $(t="closed"){if(f)return;f=t;const o=new Error(`link: session closed (${t})`);for(const i of g.values())i.reject?.(o),i.body?.fail(o);for(const i of h.values())i.body.fail(o);g.clear(),h.clear(),k=[],c(t)}async function ee(t){if(!l||h.size>=Y){j(y({t:"error",id:t.id,message:l?"too many open requests":"this side serves nothing"}));return}const o=Z(),i={body:o,cancelled:!1};h.set(t.id,i);let b=!1;const v={head:(w,L={})=>(b=!0,y({t:"res",id:t.id,status:w,headers:H(L)})),write:w=>i.cancelled?Promise.resolve():F(t.id,w instanceof Uint8Array?w:_.encode(String(w))),end:()=>(h.delete(t.id),i.cancelled?Promise.resolve():y({t:"end",id:t.id})),fail:w=>(h.delete(t.id),y({t:"error",id:t.id,message:String(w).slice(0,500)})),get cancelled(){return i.cancelled}};Promise.resolve().then(()=>l({method:t.method,path:t.path,headers:t.headers,body:o},v)).catch(w=>j(b?v.end():v.fail(w?.message||"the request failed")))}return{get closed(){return f},close:$,receive(t){const o=R.then(()=>this._receive(t)).catch(i=>$(`bad frame: ${i?.message||i}`));return R=o,o},async _receive(t){if(f)return;let o;try{o=de(await e.decrypt(t),{batch:d})}catch(i){$(/decrypt|operation|tag/i.test(String(i?.message||i?.name))?"tampered":`bad frame: ${i?.message}`);return}for(const i of o){if(f)return;await this._frame(i)}},async _frame(t){if(t.t==="ping"){j(y({t:"pong"}));return}if(t.t==="pong")return;if(t.t==="req"){if(m(t.id)){$("bad frame: a request in our id space");return}await ee(t);return}const o=m(t.id)?g.get(t.id):null,i=m(t.id)?null:h.get(t.id);if(t.t==="res"&&o)o.resolve({status:t.status,headers:t.headers||{},body:o.body});else if(t.t==="body"&&(o||i))(o||i).body.push(ae(t.data));else if(t.t==="end"&&o)o.body.end(),g.delete(t.id);else if(t.t==="end"&&i)i.body.end();else if(t.t==="error"&&o){const b=new Error(t.message||"the request failed");o.reject(b),o.body.fail(b),g.delete(t.id)}else t.t==="cancel"&&i&&(i.cancelled=!0,i.body.fail(new Error("cancelled")),h.delete(t.id))},request({method:t="GET",path:o,headers:i={},body:b=null,signal:v=null}={}){if(!W.includes(t))return Promise.reject(new Error("link: method not allowed"));if(!Q(o))return Promise.reject(new Error("link: path not allowed"));if(g.size>=Y)return Promise.reject(new Error("link: too many open requests"));const w=p;p+=2;const L=Z(),te=new Promise((x,A)=>g.set(w,{resolve:x,reject:A,body:L}));return v?.addEventListener?.("abort",()=>{const x=g.get(w);if(!x)return;g.delete(w);const A=new Error("aborted");x.reject(A),x.body.fail(A),y({t:"cancel",id:w}).catch(()=>{})},{once:!0}),(async()=>{await y({t:"req",id:w,method:t,path:o,headers:H(i)}),b!=null&&await F(w,b instanceof Uint8Array?b:_.encode(String(b))),await y({t:"end",id:w})})().catch(x=>{const A=g.get(w);A&&(A.reject(x),g.delete(w))}),te},ping:()=>y({t:"ping"})}}export{G as BODY_CHUNK_BYTES,we as LINK_CAPS,q as LINK_COALESCE,W as LINK_METHODS,V as LINK_PACE,U as LINK_SCHEME,C as LINK_VERSION,P as MAX_FRAME_BYTES,me as admissionToken,ye as admissionVerifier,fe as coalesceChunks,Ee as createLinkSession,be as decodeFrame,de as decodeFrames,ue as encodeBatch,ce as encodeFrame,oe as fromB64url,ne as linkPacer,Q as linkPathAllowed,pe as pairingUri,ge as parsePairingUri,H as sanitizeLinkHeaders,S as toB64url};
1
+ import{concatBytes as W}from"./link-noise.js";const C=1,H="chatpanel-link",_=6e4,X=44e3,Y=Object.freeze(["GET","POST","PUT","PATCH","DELETE"]),V=64,me=Object.freeze(["batch"]),se=2*_,Q=Object.freeze({bytesPerSecond:9e5,byteBurst:6e6,framesPerSecond:90,frameBurst:800});function le({bytesPerSecond:e,byteBurst:t,framesPerSecond:r,frameBurst:i}=Q,{now:d=()=>Date.now(),sleep:c=o=>new Promise(l=>setTimeout(l,o))}={}){let o=t,l=i,u=d();return async E=>{for(;;){const p=d(),m=Math.max(0,p-u)/1e3;if(u=p,o=Math.min(t,o+m*e),l=Math.min(i,l+m*r),o>=E&&l>=1){o-=E,l-=1;return}const f=Math.max((E-o)/e,(1-l)/r)*1e3;await c(Math.max(1,Math.ceil(f)))}}}const A=new TextEncoder,N=new TextDecoder,R=Object.freeze({firstMs:0,windowMs:100,maxBytes:16e3});async function*ye(e,{firstMs:t=R.firstMs,windowMs:r=R.windowMs,maxBytes:i=R.maxBytes,setTimer:d=setTimeout,clearTimer:c=clearTimeout}={}){const o=e[Symbol.asyncIterator]();let l=[],u=0,E=!0,p=null;const m=()=>{const f=l.length===1?l[0]:W(...l);return l=[],u=0,E=!1,f};try{for(;;){if(p||=o.next(),!u){const h=await p;if(p=null,h.done)return;if(l.push(h.value),u+=h.value.length,E&&t<=0){yield m();continue}}let f=null;const g=new Promise(h=>{f=d(()=>h(null),E?t:r)});try{for(;u<i;){p||=o.next();const h=await Promise.race([p,g]);if(h===null)break;if(p=null,h.done){yield m();return}l.push(h.value),u+=h.value.length}}finally{c(f)}yield m()}}finally{p&&p.catch(()=>{});try{o.return?.()?.catch?.(()=>{})}catch{}}}function b(e){let t="";for(const r of e)t+=String.fromCharCode(r);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function Z(e){const t=String(e||"").replace(/-/g,"+").replace(/_/g,"/");if(!/^[A-Za-z0-9+/]*$/.test(t))throw new Error("link: not base64url");const r=atob(t+"=".repeat((4-t.length%4)%4));return Uint8Array.from(r,i=>i.charCodeAt(0))}const ce=e=>{let t="";for(const r of e)t+=String.fromCharCode(r);return btoa(t)},de=e=>Uint8Array.from(atob(String(e||"")),t=>t.charCodeAt(0));function ke({relay:e=null,gw:t=null,room:r,key:i,secret:d,name:c=""}){const o=te({v:String(C),relay:e,gw:t,room:b(r),key:b(i),psk:b(d),name:c}),l=new URLSearchParams({v:String(C)});return o.relay&&l.set("relay",o.relay),l.set("room",b(o.room)),l.set("key",b(o.key)),l.set("psk",b(o.secret)),o.gw&&l.set("gw",o.gw),o.name&&l.set("name",o.name),`${H}:?${l.toString()}`}function ee(e){const t=String(e||"").trim();if(!t.startsWith(`${H}:?`))throw new Error("link: not a ChatPanel pairing code");const r=new URLSearchParams(t.slice(H.length+2));return te(Object.fromEntries(r.entries()))}function te(e){if(String(e.v)!==String(C))throw new Error(`link: pairing code version ${e.v} is not supported — update the app`);const t=(o,l)=>{let u;try{u=new URL(String(o))}catch{throw new Error(`link: the pairing code's ${l} address is not readable`)}if(u.protocol!=="https:"||u.username||u.password)throw new Error(`link: the ${l} must be an https address`);return u.origin},r=e.relay?t(e.relay,"relay"):null,i=e.gw?t(e.gw,"tunnel"):null;if(!r&&!i)throw new Error("link: the pairing code has no relay address");const d=(o,l)=>{let u;try{u=Z(e[o])}catch{throw new Error(`link: ${o} is not readable`)}if(u.length!==l)throw new Error(`link: ${o} must be ${l} bytes`);return u},c=String(e.name||"").replace(/[\u0000-\u001f\u007f<>]/g,"").trim().slice(0,64);return{relay:r,gw:i,room:d("room",16),key:d("key",32),secret:d("psk",32),name:c}}const O="cplink1.",re=["link","relay","tailscale","cloudflare"],M=e=>e==="tailscale"||e==="cloudflare";function Ee({uri:e,expiresAt:t,route:r=null}){const i=ee(e);if(!Number.isFinite(t)||t<=0)throw new Error("link: a pairing code needs its expiry");if(r!=null&&!re.includes(r))throw new Error(`link: unknown route "${r}"`);if(M(r)&&!i.gw)throw new Error("link: a tunnel route needs the tunnel address");if(r&&!M(r)&&!i.relay)throw new Error("link: a relay route needs the relay address");return O+b(A.encode(`${e}&exp=${Math.floor(t)}${r?`&route=${r}`:""}`))}function be(e,{now:t=Date.now()}={}){const r=String(e||"").trim();if(!r.startsWith(O))throw new Error(`link: not a ChatPanel Link code (it starts with ${O})`);let i;try{i=N.decode(Z(r.slice(O.length)))}catch{throw new Error("link: the code is not readable — copy all of it")}const d=new URLSearchParams(i.split("?")[1]||""),c=Number(d.get("exp"));if(!Number.isFinite(c)||c<=0)throw new Error("link: the code has no expiry — ask for a new one");const o=d.get("route")||null;if(o&&!re.includes(o))throw new Error(`link: the code names a route this version does not know (${o}) — update`);const l=ee(i);if(M(o)&&!l.gw)throw new Error("link: the code’s tunnel route has no tunnel address");if(o&&!M(o)&&!l.relay)throw new Error("link: the code’s relay route has no relay address");if(t>=c)throw Object.assign(new Error("link: this code expired — ask the computer’s owner for a new one"),{code:"expired"});return{...l,expiresAt:c,route:o}}async function xe(e,t,r,i=globalThis.crypto.subtle){if(r!=="laptop"&&r!=="phone")throw new Error("link: side is laptop or phone");const d=await i.importKey("raw",e,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),c=await i.sign("HMAC",d,A.encode(`chatpanel-link admit v${C}|${b(t)}|${r}`));return b(new Uint8Array(c))}async function Se(e,t=globalThis.crypto.subtle){const r=new Uint8Array(await t.digest("SHA-256",A.encode(String(e))));return Array.from(r,i=>i.toString(16).padStart(2,"0")).join("")}const ue=/^(authorization|cookie|set-cookie|www-authenticate|host|proxy-.*|x-forwarded-.*|forwarded|x-real-ip|cf-.*|tailscale-.*|x-chatpanel-device|x-chatpanel-token|content-length|connection|transfer-encoding|upgrade)$/i;function z(e={}){const t={};for(const[r,i]of Object.entries(e||{})){const d=String(r).toLowerCase();!/^[a-z0-9-]{1,64}$/.test(d)||ue.test(d)||(t[d]=String(i).replace(/[\r\n]+/g," ").slice(0,2048))}return t}function ne(e){const t=String(e||"");if(!t.startsWith("/v1/")||t.length>2048)return!1;const r=t.split("?")[0];return!/(^|\/)\.\.?(\/|$)|%2e|%2f|%5c|\\|\/\//i.test(r)}const he=new Set(["req","body","end","res","cancel","error","ping","pong"]);function we(e){const t=A.encode(JSON.stringify(e));if(t.length>_)throw new Error("link: frame too large");return t}function fe(e){if(!e.length)throw new Error("link: an empty batch");const t=e.reduce((d,c)=>d+c.length+1,1);if(t>_)throw new Error("link: batch too large");const r=new Uint8Array(t);r[0]=91;let i=1;return e.forEach((d,c)=>{r.set(d,i),i+=d.length,r[i++]=c===e.length-1?93:44}),r}function Pe(e){if(e.length>_)throw new Error("link: frame too large");let t;try{t=JSON.parse(N.decode(e))}catch{throw new Error("link: frame is not JSON")}return B(t)}function pe(e,{batch:t=!1}={}){if(e.length>_)throw new Error("link: frame too large");let r;try{r=JSON.parse(N.decode(e))}catch{throw new Error("link: frame is not JSON")}if(!Array.isArray(r))return[B(r)];if(!t)throw new Error("link: a batch this session did not agree to");if(!r.length)throw new Error("link: an empty batch");return r.map(B)}function B(e){if(!e||typeof e!="object"||Array.isArray(e)||!he.has(e.t))throw new Error("link: unknown frame");if(e.t!=="ping"&&e.t!=="pong"&&!(Number.isInteger(e.id)&&e.id>0))throw new Error("link: frame without an id");if(e.t==="req"){if(!Y.includes(e.method))throw new Error("link: method not allowed");if(!ne(e.path))throw new Error("link: path not allowed");e.headers=z(e.headers)}if(e.t==="res"&&!(Number.isInteger(e.status)&&e.status>=100&&e.status<600))throw new Error("link: bad status");if(e.t==="body"&&typeof e.data!="string")throw new Error("link: body without data");return e}function oe(){const e=[];let t=null,r=!1,i=null;const d=()=>{const c=t;t=null,c?.()};return{push(c){e.push(c),d()},end(){r=!0,d()},fail(c){i=c,r=!0,d()},async*[Symbol.asyncIterator](){for(;;){if(e.length){yield e.shift();continue}if(i)throw i;if(r)return;await new Promise(c=>{t=c})}},async bytes(){const c=[];for await(const o of this)c.push(o);return W(...c)},async text(){return N.decode(await this.bytes())}}}function _e({transport:e,send:t,side:r,onRequest:i=null,onClose:d=()=>{},pace:c=Q,now:o,sleep:l,batch:u=!1,defer:E=p=>setTimeout(p,0)}={}){if(!e||typeof t!="function")throw new Error("link: transport and send are required");if(r!=="phone"&&r!=="laptop")throw new Error("link: side is phone or laptop");let p=r==="phone"?1:2;const m=n=>n%2===(r==="phone"?1:0);let f=null;const g=new Map,h=new Map;let U=Promise.resolve();const D=c?le(c,{...o?{now:o}:{},...l?{sleep:l}:{}}):null,K=n=>{const a=U.then(async()=>{if(f)return;const s=await e.encrypt(n);D&&await D(s.length),f||t(s)});return U=a.catch(()=>{}),a};let x=[],$=1,L=null,j=0;const F=()=>{if(L=null,!x.length||f){x=[],$=1;return}const n=x.length===1?x[0]:fe(x);x=[],$=1,j+=n.length,K(n).finally(()=>{j-=n.length}).catch(()=>{})},y=n=>{if(f)return Promise.reject(new Error(`link: session closed (${f})`));const a=we(n);return u?($+a.length+1>_&&F(),x.push(a),$+=a.length+1,L||(L=E(F)||!0),j>se?U:Promise.resolve()):K(a)},I=n=>{n?.catch?.(()=>{})};let J=Promise.resolve();const G=async(n,a)=>{for(let s=0;s<a.length;s+=X)await y({t:"body",id:n,data:ce(a.subarray(s,s+X))})};function T(n="closed"){if(f)return;f=n;const a=new Error(`link: session closed (${n})`);for(const s of g.values())s.reject?.(a),s.body?.fail(a);for(const s of h.values())s.body.fail(a);g.clear(),h.clear(),x=[],d(n)}async function ie(n){if(!i||h.size>=V){I(y({t:"error",id:n.id,message:i?"too many open requests":"this side serves nothing"}));return}const a=oe(),s={body:a,cancelled:!1};h.set(n.id,s);let k=!1;const v={head:(w,q={})=>(k=!0,y({t:"res",id:n.id,status:w,headers:z(q)})),write:w=>s.cancelled?Promise.resolve():G(n.id,w instanceof Uint8Array?w:A.encode(String(w))),end:()=>(h.delete(n.id),s.cancelled?Promise.resolve():y({t:"end",id:n.id})),fail:w=>(h.delete(n.id),y({t:"error",id:n.id,message:String(w).slice(0,500)})),get cancelled(){return s.cancelled}};Promise.resolve().then(()=>i({method:n.method,path:n.path,headers:n.headers,body:a},v)).catch(w=>I(k?v.end():v.fail(w?.message||"the request failed")))}return{get closed(){return f},close:T,receive(n){const a=J.then(()=>this._receive(n)).catch(s=>T(`bad frame: ${s?.message||s}`));return J=a,a},async _receive(n){if(f)return;let a;try{a=pe(await e.decrypt(n),{batch:u})}catch(s){T(/decrypt|operation|tag/i.test(String(s?.message||s?.name))?"tampered":`bad frame: ${s?.message}`);return}for(const s of a){if(f)return;await this._frame(s)}},async _frame(n){if(n.t==="ping"){I(y({t:"pong"}));return}if(n.t==="pong")return;if(n.t==="req"){if(m(n.id)){T("bad frame: a request in our id space");return}await ie(n);return}const a=m(n.id)?g.get(n.id):null,s=m(n.id)?null:h.get(n.id);if(n.t==="res"&&a)a.resolve({status:n.status,headers:n.headers||{},body:a.body});else if(n.t==="body"&&(a||s))(a||s).body.push(de(n.data));else if(n.t==="end"&&a)a.body.end(),g.delete(n.id);else if(n.t==="end"&&s)s.body.end();else if(n.t==="error"&&a){const k=new Error(n.message||"the request failed");a.reject(k),a.body.fail(k),g.delete(n.id)}else n.t==="cancel"&&s&&(s.cancelled=!0,s.body.fail(new Error("cancelled")),h.delete(n.id))},request({method:n="GET",path:a,headers:s={},body:k=null,signal:v=null}={}){if(!Y.includes(n))return Promise.reject(new Error("link: method not allowed"));if(!ne(a))return Promise.reject(new Error("link: path not allowed"));if(g.size>=V)return Promise.reject(new Error("link: too many open requests"));const w=p;p+=2;const q=oe(),ae=new Promise((S,P)=>g.set(w,{resolve:S,reject:P,body:q}));return v?.addEventListener?.("abort",()=>{const S=g.get(w);if(!S)return;g.delete(w);const P=new Error("aborted");S.reject(P),S.body.fail(P),y({t:"cancel",id:w}).catch(()=>{})},{once:!0}),(async()=>{await y({t:"req",id:w,method:n,path:a,headers:z(s)}),k!=null&&await G(w,k instanceof Uint8Array?k:A.encode(String(k))),await y({t:"end",id:w})})().catch(S=>{const P=g.get(w);P&&(P.reject(S),g.delete(w))}),ae},ping:()=>y({t:"ping"})}}export{X as BODY_CHUNK_BYTES,me as LINK_CAPS,R as LINK_COALESCE,Y as LINK_METHODS,Q as LINK_PACE,H as LINK_SCHEME,C as LINK_VERSION,_ as MAX_FRAME_BYTES,O as PAIRING_CODE_PREFIX,xe as admissionToken,Se as admissionVerifier,ye as coalesceChunks,_e as createLinkSession,Pe as decodeFrame,pe as decodeFrames,fe as encodeBatch,we as encodeFrame,Z as fromB64url,le as linkPacer,ne as linkPathAllowed,Ee as pairingCode,ke as pairingUri,be as parsePairingCode,ee as parsePairingUri,z as sanitizeLinkHeaders,b as toB64url};
package/link-routes.js CHANGED
@@ -1,2 +1,2 @@
1
- const o="https://link.chatpanel.net",s="/v1/link",h=`${s}/room/`,f=Object.freeze([Object.freeze({id:"link",kind:"relay",label:"ChatPanel Link",hosted:!0,summary:"Our relay at link.chatpanel.net carries the encrypted bytes. Nothing to set up."}),Object.freeze({id:"tailscale",kind:"tunnel",label:"Tailscale",summary:"Your own tailnet. Only your devices can reach it; the phone needs the Tailscale app."}),Object.freeze({id:"cloudflare",kind:"tunnel",label:"Cloudflare Tunnel",summary:"Your own Cloudflare Tunnel and a hostname you own."}),Object.freeze({id:"relay",kind:"relay",label:"Your own relay",summary:"A Link relay you run. Your server, your operations; ChatPanel only dials it."})]),c=e=>f.find(t=>t.id===e)||null,a=e=>{throw Object.assign(new Error(e),{code:"bad_route"})};function y(e,t){let n;try{n=new URL(String(e||"").trim())}catch{a(`${t}: not an address — it starts with https://`)}n.protocol!=="https:"&&a(`${t}: must be an https:// address`),(n.username||n.password)&&a(`${t}: no user name or password in the address`);const r=n.hostname.toLowerCase();return(r==="localhost"||/^[\d.]+$/.test(r)||r.includes(":"))&&a(`${t}: a name the phone can look up, not ${r}`),n.origin}function m({id:e="link",url:t=""}={}){const n=c(e);if(n||a(`unknown Link route "${e}"`),e==="link")return{id:e,url:o};const r=y(t,n.label),l=new URL(r).hostname;return e==="tailscale"&&!l.endsWith(".ts.net")&&a("Tailscale: the address `tailscale serve` prints ends in .ts.net"),e==="cloudflare"&&l.endsWith(".trycloudflare.com")&&a("Cloudflare Tunnel: a quick tunnel (trycloudflare.com) gets a new name on every start — use a named tunnel on your own hostname"),e==="relay"&&r===o?{id:"link",url:o}:{id:e,url:r}}function w(e={}){const t=e||{},n=t.relayUrl&&String(t.relayUrl).replace(/\/+$/,"")!==o?String(t.relayUrl):"",r=t.route||(n?"relay":"link");let l;try{l=m({id:r,url:r==="relay"?t.relayUrl||"":t.tunnelUrl||""})}catch(p){return{route:"link",relay:o,tunnel:null,problem:p.message}}const i=c(l.id).kind==="tunnel"?l.url:null,d=i?t.fallback===!1?null:o:l.url;return{route:l.id,relay:d,tunnel:i}}const u=e=>String(e).replace(/^https:/,"wss:").replace(/^http:/,"ws:"),g=(e,t)=>`${u(e)}/v1/room/${t}`,b=(e,t)=>`${u(e)}${h}${t}`;function $(e,{port:t=4320,url:n=""}={}){const r=`http://127.0.0.1:${Number(t)||4320}`,l=(()=>{try{return new URL(n).hostname}catch{return"link.example.com"}})();return e==="tailscale"?{commands:[`tailscale serve --bg --set-path=${s} ${r}${s}`],notes:["Run it on this computer, then paste the https://…ts.net address it prints (`tailscale serve status` shows it again).","On the phone: install Tailscale and sign in to the same tailnet. Use serve, not funnel: serve is reachable from your devices only."]}:e==="cloudflare"?{commands:["cloudflared tunnel create chatpanel-link",`cloudflared tunnel route dns chatpanel-link ${l}`,"cloudflared tunnel run chatpanel-link"],config:["tunnel: chatpanel-link","ingress:",` - hostname: ${l}`,` path: ^${s}/`,` service: ${r}`," - service: http_status:404"].join(`
2
- `),notes:["The config goes in ~/.cloudflared/config.yml (add the credentials-file line `tunnel create` prints). Only the Link path is mapped; everything else answers 404.","Cloudflare Access in front is an optional extra wall — the phone must then pass it, so leave it off unless you know you want it."]}:e==="relay"?{commands:[],notes:["Give the https address of your relay. It must speak the Link relay wire (rooms at /v1/room/<room>, the admission token as a WebSocket subprotocol)."]}:{commands:[],notes:[]}}const k=/^(cf-connecting-ip|cf-ray|cf-ipcountry|cf-visitor|cf-warp-tag-id|cdn-loop|x-forwarded-for|x-forwarded-host|x-forwarded-proto|x-real-ip|forwarded|tailscale-user-login|tailscale-user-name|tailscale-user-profile-pic|tailscale-headers-info|tailscale-app-capabilities|tailscale-funnel-request)$/i;function L(e={}){return(typeof e?.keys=="function"&&!Array.isArray(e)?[...e.keys()]:Object.keys(e||{})).find(n=>k.test(n))||null}export{o as DEFAULT_LINK_RELAY,f as LINK_ROUTES,s as LINK_TUNNEL_PATH,h as LINK_TUNNEL_ROOM_PATH,m as checkLinkRoute,w as linkPaths,c as linkRoute,g as relaySocketUrl,L as tunnelHeader,$ as tunnelSetup,b as tunnelSocketUrl};
1
+ const o="https://link.chatpanel.net",s="/v1/link",p=`${s}/room/`,y=Object.freeze([Object.freeze({id:"link",kind:"relay",label:"ChatPanel Link",hosted:!0,summary:"Our relay at link.chatpanel.net carries the encrypted bytes. Nothing to set up.",reach:"Reachable from anywhere; both sides dial out to our relay, which ChatPanel runs and pays for per message."}),Object.freeze({id:"tailscale",kind:"tunnel",label:"Tailscale",summary:"Your own tailnet. Only your devices can reach it; the phone needs the Tailscale app.",reach:"Reachable only from devices on your tailnet — a partner server must join it. No relay."}),Object.freeze({id:"cloudflare",kind:"tunnel",label:"Cloudflare Tunnel",summary:"Your own Cloudflare Tunnel and a hostname you own.",reach:"Reachable from anywhere at your hostname, through your own Cloudflare account. No relay."}),Object.freeze({id:"relay",kind:"relay",label:"Your own relay",summary:"A Link relay you run. Your server, your operations; ChatPanel only dials it.",reach:"Reachable from anywhere your relay is; you run it and carry its cost."})]),c=e=>y.find(t=>t.id===e)||null,l=e=>{throw Object.assign(new Error(e),{code:"bad_route"})};function f(e,t){let n;try{n=new URL(String(e||"").trim())}catch{l(`${t}: not an address — it starts with https://`)}n.protocol!=="https:"&&l(`${t}: must be an https:// address`),(n.username||n.password)&&l(`${t}: no user name or password in the address`);const r=n.hostname.toLowerCase();return(r==="localhost"||/^[\d.]+$/.test(r)||r.includes(":"))&&l(`${t}: a name the phone can look up, not ${r}`),n.origin}function m({id:e="link",url:t=""}={}){const n=c(e);if(n||l(`unknown Link route "${e}"`),e==="link")return{id:e,url:o};const r=f(t,n.label),a=new URL(r).hostname;return e==="tailscale"&&!a.endsWith(".ts.net")&&l("Tailscale: the address `tailscale serve` prints ends in .ts.net"),e==="cloudflare"&&a.endsWith(".trycloudflare.com")&&l("Cloudflare Tunnel: a quick tunnel (trycloudflare.com) gets a new name on every start — use a named tunnel on your own hostname"),e==="relay"&&r===o?{id:"link",url:o}:{id:e,url:r}}function w(e={}){const t=e||{},n=t.relayUrl&&String(t.relayUrl).replace(/\/+$/,"")!==o?String(t.relayUrl):"",r=t.route||(n?"relay":"link");let a;try{a=m({id:r,url:r==="relay"?t.relayUrl||"":t.tunnelUrl||""})}catch(d){return{route:"link",relay:o,tunnel:null,problem:d.message}}const i=c(a.id).kind==="tunnel"?a.url:null,h=i?t.fallback===!1?null:o:a.url;return{route:a.id,relay:h,tunnel:i}}const u=e=>String(e).replace(/^https:/,"wss:").replace(/^http:/,"ws:"),b=(e,t)=>`${u(e)}/v1/room/${t}`,g=(e,t)=>`${u(e)}${p}${t}`;function $(e,{port:t=4320,url:n=""}={}){const r=`http://127.0.0.1:${Number(t)||4320}`,a=(()=>{try{return new URL(n).hostname}catch{return"link.example.com"}})();return e==="tailscale"?{commands:[`tailscale serve --bg --set-path=${s} ${r}${s}`],notes:["Run it on this computer, then paste the https://…ts.net address it prints (`tailscale serve status` shows it again).","On the phone: install Tailscale and sign in to the same tailnet. Use serve, not funnel: serve is reachable from your devices only."]}:e==="cloudflare"?{commands:["cloudflared tunnel create chatpanel-link",`cloudflared tunnel route dns chatpanel-link ${a}`,"cloudflared tunnel run chatpanel-link"],config:["tunnel: chatpanel-link","ingress:",` - hostname: ${a}`,` path: ^${s}/`,` service: ${r}`," - service: http_status:404"].join(`
2
+ `),notes:["The config goes in ~/.cloudflared/config.yml (add the credentials-file line `tunnel create` prints). Only the Link path is mapped; everything else answers 404.","Cloudflare Access in front is an optional extra wall — the phone must then pass it, so leave it off unless you know you want it."]}:e==="relay"?{commands:[],notes:["Give the https address of your relay. It must speak the Link relay wire (rooms at /v1/room/<room>, the admission token as a WebSocket subprotocol)."]}:{commands:[],notes:[]}}const k=/^(cf-connecting-ip|cf-ray|cf-ipcountry|cf-visitor|cf-warp-tag-id|cdn-loop|x-forwarded-for|x-forwarded-host|x-forwarded-proto|x-real-ip|forwarded|tailscale-user-login|tailscale-user-name|tailscale-user-profile-pic|tailscale-headers-info|tailscale-app-capabilities|tailscale-funnel-request)$/i;function v(e={}){return(typeof e?.keys=="function"&&!Array.isArray(e)?[...e.keys()]:Object.keys(e||{})).find(n=>k.test(n))||null}export{o as DEFAULT_LINK_RELAY,y as LINK_ROUTES,s as LINK_TUNNEL_PATH,p as LINK_TUNNEL_ROOM_PATH,m as checkLinkRoute,w as linkPaths,c as linkRoute,b as relaySocketUrl,v as tunnelHeader,$ as tunnelSetup,g as tunnelSocketUrl};
package/link-scopes.js ADDED
@@ -0,0 +1 @@
1
+ const C=Object.freeze(["phone","partner"]),d=e=>e?.kind==="partner"?"partner":"phone",l=Object.freeze({models:Object.freeze({label:"see which models this computer offers",routes:Object.freeze([Object.freeze({method:"GET",path:/^\/v1\/models(?:\/|$|\?)/})])}),chat:Object.freeze({label:"chat with this computer’s API models (redacted, as every turn is) — not its coding agents",routes:Object.freeze([Object.freeze({method:"POST",path:/^\/v1\/chat\/completions(?:$|\?)/}),Object.freeze({method:"POST",path:/^\/v1\/messages(?:$|\?)/})])}),agents:Object.freeze({label:"also run this computer’s coding agents (Claude Code, Codex, …) as they run here — your Coding Agents settings, sandbox and permission mode decide what they may do",routes:Object.freeze([])}),files:Object.freeze({label:"put data, skills, subagents and instructions in its own folder, and read back what its agents write there — never files that configure the agent",routes:Object.freeze([Object.freeze({method:"GET",path:/^\/v1\/link\/files(?:\/|$|\?)/}),Object.freeze({method:"PUT",path:/^\/v1\/link\/files\/[^?]+$/}),Object.freeze({method:"DELETE",path:/^\/v1\/link\/files\/[^?]+$/})])})}),u=Object.freeze(Object.keys(l)),f=Object.freeze(["models","chat"]),i=e=>{throw Object.assign(new Error(e),{code:"bad_partner"})};function h(e=f){const t=(Array.isArray(e)?e:String(e??"").split(",")).map(n=>String(n||"").trim().toLowerCase()).filter(Boolean);if(!t.length)return[...f];for(const n of t)l[n]||i(`unknown scope "${n}" — one of ${u.join(", ")}`);return t.includes("agents")&&!t.includes("chat")&&i('"agents" widens "chat" — grant chat too (--scopes models,chat,agents)'),t.includes("files")&&!t.includes("agents")&&i('"files" is the agents’ folder — grant agents too (--scopes models,chat,agents,files)'),u.filter(n=>t.includes(n))}function g(e){const r=String(e??"").trim().replace(/\s+/g," ");return/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,39}$/.test(r)||i("a partner name is 1–40 letters, digits, spaces, dots, dashes or underscores, starting with a letter or digit"),r}const S=e=>String(e||"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40)||"partner";function A(e,r){const t=String(r||"").replace(/[\\/]+$/,""),n=/^[A-Za-z]:\\/.test(t)?"\\":"/";return[t,".chatpanel","partners",S(e)].join(n)}function _(e,{name:r,home:t}={}){let n=String(e??"").trim();if(!n)return A(r,t);const o=String(t||"").replace(/[\\/]+$/,"");(n==="~"||n.startsWith("~/"))&&(n=o+n.slice(1)),/^(\/|[A-Za-z]:[\\/])/.test(n)||i("a partner folder is an absolute path (or starts with ~/)"),n.split(/[\\/]+/).includes("..")&&i('a partner folder may not contain ".."');const a=n.replace(/[\\/]+$/,"");return(!a||/^[A-Za-z]:$/.test(a)||a===o)&&i("a partner folder must be a folder of its own, not the whole disk or your home folder"),a}const m=e=>Array.isArray(e?.scopes)?e.scopes.filter(r=>l[r]):[];function z(e,{method:r="GET",path:t=""}={}){const n=String(r).toUpperCase(),o=String(t||"");return(Array.isArray(e)?e:[]).some(s=>l[s]?.routes.some(c=>c.method===n&&c.path.test(o)))}function R(e,r){if(!e||d(e)!=="partner")return!0;const t=m(e);return t.includes("chat")?r==="api"||t.includes("agents"):!1}const x=/^(content-type|accept|accept-language|anthropic-version|anthropic-beta|openai-beta|x-request-id|user-agent|x-chatpanel-destination|x-chatpanel-run)$/i;function P(e={}){const r={};for(const[t,n]of Object.entries(e||{}))x.test(t)&&(r[t.toLowerCase()]=String(n));return r}const E="x-chatpanel-link-device",y="x-chatpanel-link-kind",k="x-chatpanel-link-partner",b="x-chatpanel-link-scopes",$=/^x-chatpanel-link-/i;function T(e={}){const r={};for(const[t,n]of Object.entries(e||{}))$.test(t)||(r[t]=n);return r}function D(e){return d(e)!=="partner"?{}:{[y]:"partner",[k]:encodeURIComponent(String(e.partner?.name||e.name||"partner")),[b]:m(e).join(",")}}function I(e={}){const r=s=>{if(typeof e?.get=="function")return e.get(s);for(const[c,p]of Object.entries(e||{}))if(c.toLowerCase()===s)return Array.isArray(p)?p[0]:p},t=String(r(E)||""),n=String(r(y)||"");if(!t&&n!=="partner")return null;if(n!=="partner")return{device:t,kind:"phone"};let o="";try{o=decodeURIComponent(String(r(k)||""))}catch{o=""}const a=String(r(b)||"").split(",").map(s=>s.trim()).filter(s=>l[s]);return{device:t,kind:"partner",partner:o.slice(0,40)||"partner",scopes:a}}function N(e){return e?e.kind==="partner"?`partner:${e.partner}`:"phone":null}function L(e,r){const t={},n=String(r||"").replace(/[^A-Za-z0-9_-]/g,"").slice(0,40),o=typeof e?.thread?.id=="string"?e.thread.id:"",a=n&&o?`partner.${n}.${o}`:"";if(a&&/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(a)){const s=typeof e.thread.title=="string"?e.thread.title:"";t.thread={id:a,...s?{title:s}:{}}}return e?.ephemeral===!0&&(t.ephemeral=!0),t}const O={link:"ChatPanel’s hosted relay — both sides dial out to it; it carries only ciphertext",relay:"your own relay — both sides dial out to it; it carries only ciphertext",tailscale:"your Tailscale tunnel door on this computer — no relay; the partner’s server must be on your tailnet",cloudflare:"your Cloudflare Tunnel door on this computer — no relay; reachable from anywhere at your hostname"};function F({partner:e,scopes:r,route:t="link",host:n="",folder:o=""}={}){const a=g(e),s=h(r),c=[`Partner "${a}" will reach this computer's gateway over ChatPanel Link, end-to-end encrypted.`,`Route: ${t} — its server connects to ${n||"(no host)"}: ${O[t]||t}.`,"It will be able to:",...s.map(p=>` • ${l[p].label}`),`Coding agents: ${s.includes("agents")?"YES — they run as they do here: your Coding Agents settings, sandbox and permission mode decide what they may do; the partner never answers their approval prompts":"no"}.`,...s.includes("agents")&&o?[`Its agents work in ${o} — its own skills (.claude/skills), subagents (.claude/agents) and instructions (CLAUDE.md, AGENTS.md) go there.`]:[],`Files: ${s.includes("files")?"YES — it may put data (data/), skills, subagents and instructions in that folder and read them back; never settings, hooks or MCP config":"no"}.`,"It will never reach pairing, settings, prefs, history, memory or the event log.","The code works once, within 10 minutes. Revoke it any time: chatpanel-gateway link revoke <id>, or Revoke in Settings › Link › Servers."];return{partner:a,scopes:s,agents:s.includes("agents"),route:t,host:n,...o?{folder:o}:{},lines:c}}function K(){return u.map(e=>({id:e,label:l[e].label,checked:f.includes(e),needs:e==="agents"?"chat":e==="files"?"agents":null}))}function U({name:e,scopes:r,route:t,relay:n,folder:o,confirm:a=!1}={}){const s={kind:"partner",partner:{name:g(e)},scopes:h(r)};return typeof o=="string"&&o.trim()&&(s.folder=o.trim().slice(0,1024)),t&&(s.route=String(t)),t==="relay"&&n&&(s.relay=String(n).trim()),a===!0&&(s.confirm=!0),s}const w=[/^data\/./,/^\.claude\/skills\/./,/^\.agents\/skills\/./,/^\.claude\/agents\/[^/]+\.md$/,/^(CLAUDE|AGENTS)\.md$/],v=/^(allowed-tools|allowed_tools|allowedTools|hooks|permissionMode|permission-mode|mcpServers|mcp-servers|mcp_servers)\s*:/m;function M(e,{listing:r=!1}={}){let t=String(e??"");try{t=decodeURIComponent(t)}catch{i("a file path is not valid URL encoding")}if(t=t.replace(/^\/+/,""),r&&(t===""||t==="."))return"";(!t||t.length>512||/[\\\u0000-\u001f]/.test(t))&&i("a file path is 1–512 characters, with forward slashes and no control characters");const n=t.split("/");n.some(a=>a===""||a==="."||a==="..")&&i('a file path is relative to the partner’s folder: no empty, "." or ".." parts');const o=n.join("/");return!w.some(a=>a.test(o))&&!(r&&/^(data|\.claude\/skills|\.claude\/agents|\.agents\/skills)$/.test(o))&&i(`a partner may hold files only under data/, .claude/skills/, .agents/skills/, .claude/agents/*.md, CLAUDE.md or AGENTS.md — not "${o}"`),o}const H=e=>w.some(r=>r.test(String(e||"")));function Z(e,r){if(!/^(\.claude\/skills\/|\.agents\/skills\/|\.claude\/agents\/)/.test(String(e||""))||!/\.md$/i.test(String(e)))return;const t=/^\uFEFF?---\r?\n([\s\S]*?)\r?\n---/.exec(String(r||"")),n=t?t[1]:"",o=v.exec(n);o&&i(`${e}: "${o[1]}" in its front matter would change what the agent may do — that is the owner’s Coding Agents settings, not a partner’s file`)}const j=Object.freeze([Object.freeze({id:"allow",label:"Allow once"}),Object.freeze({id:"allow_action",label:"Allow this for the conversation"}),Object.freeze({id:"allow_all",label:"Allow everything in this conversation"}),Object.freeze({id:"deny",label:"Deny"})]);function G(e){return e==="allow"?"allow":e==="allow_action"?"always":e==="allow_all"?"all":"deny"}function V(e={},{now:r=Date.now()}={}){const t=Math.max(0,Math.round(((Number(e.expiresAt)||0)-r)/1e3));return{id:String(e.id||""),title:String(e.title||`${e.partner||"A partner"}’s agent asks`),body:String(e.body||""),partner:String(e.partner||""),left:`${Math.floor(t/60)}:${String(t%60).padStart(2,"0")}`,decisions:j}}function Y(e=[]){const r=Array.isArray(e)?e:[];return{phones:r.filter(t=>d(t)==="phone"),partners:r.filter(t=>d(t)==="partner")}}function B(e={}){const r=Array.isArray(e.scopes)?e.scopes:[];return{id:e.id,title:`${e.partner?.name||e.name||"partner"} — partner server`,detail:`may ${r.filter(t=>t!=="agents"&&t!=="files").join(" and ")||"nothing"} · coding agents: ${r.includes("agents")?`yes${e.folder?`, in ${e.folder}`:""}`:"no"}${r.includes("files")?" · files: yes":""} · route: ${e.route||"?"}${e.host?` via ${e.host}`:""}`,warning:e.routeClosed?"Its tunnel door closed when this computer’s route changed — pair it again to move it.":""}}export{f as DEFAULT_PARTNER_SCOPES,E as LINK_DEVICE_HEADER,C as LINK_DEVICE_KINDS,y as LINK_KIND_HEADER,k as LINK_PARTNER_HEADER,b as LINK_SCOPES_HEADER,j as PARTNER_APPROVAL_DECISIONS,l as PARTNER_SCOPES,u as PARTNER_SCOPE_IDS,Z as checkPartnerFileText,_ as checkPartnerFolder,g as checkPartnerName,h as checkPartnerScopes,A as defaultPartnerFolder,F as describePartnerAccess,I as linkCallerOf,R as linkDestinationAllowed,d as linkDeviceKind,D as linkMarkHeaders,N as linkVia,V as partnerApprovalCard,G as partnerDecisionForDesk,B as partnerDeviceRow,M as partnerFilePath,S as partnerFolderSlug,P as partnerHeaders,z as partnerMay,H as partnerMayHold,U as partnerPairBody,L as partnerRunHint,K as partnerScopeChoices,Y as splitLinkDevices,T as withoutLinkMarks};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/events",
3
- "version": "0.356.1",
3
+ "version": "0.358.2",
4
4
  "description": "The canonical ChatPanel event-log and capability contracts — typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -133,6 +133,8 @@
133
133
  "./link-connect.js": "./link-connect.js",
134
134
  "./link-sync.js": "./link-sync.js",
135
135
  "./link-routes.js": "./link-routes.js",
136
+ "./link-scopes.js": "./link-scopes.js",
137
+ "./link-fetch.js": "./link-fetch.js",
136
138
  "./qr.js": "./qr.js",
137
139
  "./record-entities.js": "./record-entities.js",
138
140
  "./decide-by-model.js": "./decide-by-model.js",
@@ -306,6 +308,8 @@
306
308
  "./read-document.js": "./read-document.js",
307
309
  "./serp-anchors.js": "./serp-anchors.js",
308
310
  "./search-searxng.js": "./search-searxng.js",
311
+ "./runtime-host.js": "./runtime-host.js",
312
+ "./sandbox-choice.js": "./sandbox-choice.js",
309
313
  "./sandbox-policy.js": "./sandbox-policy.js",
310
314
  "./secret-names.js": "./secret-names.js",
311
315
  "./sandbox-grant.js": "./sandbox-grant.js"
@@ -441,6 +445,8 @@
441
445
  "link-connect.js",
442
446
  "link-sync.js",
443
447
  "link-routes.js",
448
+ "link-scopes.js",
449
+ "link-fetch.js",
444
450
  "qr.js",
445
451
  "record-entities.js",
446
452
  "decide-by-model.js",
@@ -614,6 +620,8 @@
614
620
  "read-document.js",
615
621
  "serp-anchors.js",
616
622
  "search-searxng.js",
623
+ "runtime-host.js",
624
+ "sandbox-choice.js",
617
625
  "sandbox-policy.js",
618
626
  "secret-names.js",
619
627
  "sandbox-grant.js"
package/policy.js CHANGED
@@ -1 +1 @@
1
- class a extends Error{constructor(n){super(n),this.name="PolicyError"}}const x=1,S="policy",d=Object.freeze(["off","basic","full"]),A=Object.freeze({off:0,basic:1,full:2}),O=Object.freeze(["gateway","endpoints","redaction","features","agents"]),k=Object.freeze(["webllm","meetings","channels","agents"]),y=Object.freeze(["plan","default","acceptEdits","bypassPermissions"]),E=Object.freeze({plan:0,default:1,acceptEdits:2,bypassPermissions:3}),T=Object.freeze(["api.chatpanel.net","dl.chatpanel.net","huggingface.co","registry.npmjs.org","api.github.com"]),f=e=>typeof e=="string"?e.trim():"",c=e=>Array.isArray(e)?e.map(f).filter(Boolean):[];function h(e){const n=f(e).toLowerCase().replace(/^https?:\/\//,"").replace(/\/.*$/,"");if(!n)return"";if(!/^(\*\.)?[a-z0-9.-]+(:\d{1,5})?$/.test(n)&&!/^\[[0-9a-f:]+\](:\d{1,5})?$/.test(n)&&n!=="*")throw new a(`invalid host pattern "${e}"`);return n}function b(e){if(e==null)return b({});if(typeof e!="object"||Array.isArray(e))throw new a("policy must be an object");const n=e.v==null?x:Number(e.v);if(n!==x)throw new a(`unsupported policy version ${e.v}`);const t={v:n};if(e.org!=null){const r=f(e.org.id);if(!r||!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(r))throw new a("org.id must be a short identifier");t.org={id:r,name:f(e.org.name)||r}}const o=(r,s)=>{if(r==null)return;if(typeof r!="object")throw new a(`${s} must be an object`);const u={};return r.allow!=null&&(u.allow=c(r.allow).map(h).filter(Boolean)),r.deny!=null&&(u.deny=c(r.deny).map(h).filter(Boolean)),u},i=o(e.endpoints,"endpoints");i&&(t.endpoints=i);const l=o(e.egress,"egress");if(l&&(t.egress=l),e.models!=null){if(typeof e.models!="object")throw new a("models must be an object");t.models={},e.models.allow!=null&&(t.models.allow=c(e.models.allow)),e.models.deny!=null&&(t.models.deny=c(e.models.deny))}if(e.agents!=null){if(typeof e.agents!="object")throw new a("agents must be an object");const r={};if(e.agents.allow!=null&&(r.allow=c(e.agents.allow).map(s=>s.toLowerCase())),e.agents.deny!=null&&(r.deny=c(e.agents.deny).map(s=>s.toLowerCase())),e.agents.permissionModeCap!=null){const s=f(e.agents.permissionModeCap);if(!y.includes(s))throw new a(`agents.permissionModeCap must be one of ${y.join(", ")}`);r.permissionModeCap=s}t.agents=r}if(e.routing!=null){if(typeof e.routing!="object")throw new a("routing must be an object");const r=Array.isArray(e.routing.rules)?e.routing.rules:[];t.routing={rules:r.map((s,u)=>{if(!s||typeof s!="object")throw new a(`routing.rules[${u}] must be an object`);const g=c(s.models),$=c(s.endpoints).map(h).filter(Boolean);if(!g.length)throw new a(`routing.rules[${u}] needs at least one model pattern`);if(!$.length)throw new a(`routing.rules[${u}] needs at least one endpoint`);return{models:g,endpoints:$}})}}if(e.redaction!=null){if(typeof e.redaction!="object")throw new a("redaction must be an object");const r={};if(e.redaction.floor!=null){const s=f(e.redaction.floor).toLowerCase();if(!d.includes(s))throw new a(`redaction.floor must be one of ${d.join(", ")}`);r.floor=s}e.redaction.dictionary!=null&&(r.dictionary=c(e.redaction.dictionary)),e.redaction.requireBeforeCloud!=null&&(r.requireBeforeCloud=!!e.redaction.requireBeforeCloud),t.redaction=r}if(e.client!=null){if(typeof e.client!="object")throw new a("client must be an object");const r={};if(e.client.gatewayUrl!=null){const s=f(e.client.gatewayUrl);if(s&&!/^https?:\/\/[^\s/]+(\/.*)?$/.test(s))throw new a("client.gatewayUrl must be an http(s) URL");r.gatewayUrl=s.replace(/\/+$/,"")}if(e.client.managedRequired!=null&&(r.managedRequired=!!e.client.managedRequired),e.client.lock!=null){const s=c(e.client.lock);for(const u of s)if(!O.includes(u))throw new a(`client.lock: unknown setting "${u}" (one of ${O.join(", ")})`);r.lock=[...new Set(s)]}if(e.client.features!=null){if(typeof e.client.features!="object")throw new a("client.features must be an object");r.features={};for(const[s,u]of Object.entries(e.client.features)){if(!k.includes(s))throw new a(`client.features: unknown feature "${s}" (one of ${k.join(", ")})`);r.features[s]=!!u}}t.client=r}if(e.access!=null){if(typeof e.access!="object"||Array.isArray(e.access))throw new a("access must be an object");const r={};if(e.access.delegation!=null){const s=f(e.access.delegation);if(!C.includes(s))throw new a(`access.delegation must be one of ${C.join(", ")}`);r.delegation=s}for(const s of["deny","allow"]){if(e.access[s]==null)continue;const u=c(e.access[s]).map(g=>String(g).trim()).filter(Boolean);for(const g of u)if(g.length>1024||g.includes("\0"))throw new a(`access.${s}: not a path`);r[s]=u}Object.keys(r).length&&(t.access=r)}return t}const C=Object.freeze(["allowed","never"]);function I(e){return e?.access?.delegation==="never"?"never":"allowed"}function p(e){const n=f(e).toLowerCase();if(!n)return{host:"",port:""};try{const t=new URL(/^[a-z][a-z0-9+.-]*:\/\//.test(n)?n:`http://${n}`);return{host:t.hostname.replace(/^\[|\]$/g,""),port:t.port||(t.protocol==="https:"?"443":t.protocol==="http:"?"80":"")}}catch{return{host:n.split(":")[0],port:""}}}function R(e,n,t){if(e==="*")return!0;const[o,i]=e.startsWith("[")?[e.replace(/^\[([^\]]+)\].*$/,"$1"),e.replace(/^\[[^\]]+\]:?/,"")]:(()=>{const l=e.lastIndexOf(":");return l>0?[e.slice(0,l),e.slice(l+1)]:[e,""]})();if(i&&i!==t)return!1;if(o.startsWith("*.")){const l=o.slice(1);return n.endsWith(l)&&n.length>l.length}return n===o}function w(e,n,t){return(e||[]).some(o=>R(o,n,t))}function m(e){return new RegExp(`^${String(e).split("*").map(n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join(".*")}$`,"i")}function U(e,n){const t=e?.endpoints;if(!t)return!0;const{host:o,port:i}=p(n);return!o||w(t.deny,o,i)?!1:t.allow&&t.allow.length?w(t.allow,o,i):!0}function z(e,n){const t=e?.models;if(!t)return!0;const o=f(n);return(t.deny||[]).some(i=>m(i).test(o))?!1:t.allow&&t.allow.length?t.allow.some(i=>m(i).test(o)):!0}function L(e,n){const t=e?.agents;if(!t)return!0;const o=f(n).toLowerCase();return(t.deny||[]).includes(o)?!1:t.allow&&t.allow.length?t.allow.includes(o):!0}function N(e,n){const t=e?.agents?.permissionModeCap,o=y.includes(n)?n:"default";return t?E[o]<=E[t]?o:t:o}function _(e,{model:n="",endpoint:t=""}={}){if(!e)return{ok:!0,reason:""};if(n&&!z(e,n))return{ok:!1,reason:`model "${n}" is not allowed by your organisation's policy`};if(t&&!U(e,t))return{ok:!1,reason:`endpoint ${p(t).host} is not allowed by your organisation's policy`};const o=e.routing?.rules||[];if(n&&t&&o.length){const i=o.find(l=>l.models.some(r=>m(r).test(n)));if(i){const{host:l,port:r}=p(t);if(!w(i.endpoints,l,r))return{ok:!1,reason:`model "${n}" may only be sent to ${i.endpoints.join(", ")} under your organisation's policy`}}}return{ok:!0,reason:""}}function P(e,n){const t=e?.redaction?.floor,o=d.includes(n)?n:"basic";return t?A[o]>=A[t]?o:t:o}function B(e,n,{local:t=!1}={}){const o=e?.egress;if(!o)return!0;const{host:i,port:l}=p(n);return!i||w(o.deny,i,l)?!1:t||T.includes(i)||e.endpoints?.allow?.length&&w(e.endpoints.allow,i,l)&&!w(e.endpoints?.deny,i,l)?!0:w(o.allow,i,l)}function q(e,n){return!!e?.client?.lock?.includes(n)}function M(e,n){const t=e?.client?.features;return!t||t[n]==null?!0:!!t[n]}function v(e,n){const t={...n||{}},o=[];if(!e)return{settings:t,changed:o};const i=e.client||{};if(i.gatewayUrl&&t.gatewayUrl!==i.gatewayUrl&&(t.gatewayUrl=i.gatewayUrl,o.push("gatewayUrl")),Array.isArray(t.endpoints)&&e.endpoints){const l=t.endpoints.filter(r=>!r||typeof r!="object"?!1:!r.baseUrl||i.gatewayUrl&&p(r.baseUrl).host===p(i.gatewayUrl).host&&p(r.baseUrl).port===p(i.gatewayUrl).port?!0:U(e,r.baseUrl));l.length!==t.endpoints.length&&(t.endpoints=l,o.push("endpoints"))}if(Array.isArray(t.endpoints)&&e.agents){const l=t.endpoints.filter(r=>r?.kind!=="bridge"||L(e,r.bridgeAgent||r.agent||r.id));l.length!==t.endpoints.length&&(t.endpoints=l,o.push("agents"))}if(Array.isArray(t.endpoints)&&!M(e,"webllm")){const l=t.endpoints.filter(r=>r?.kind!=="webllm");l.length!==t.endpoints.length&&(t.endpoints=l,o.push("webllm"))}if(e.redaction?.floor&&(t.redaction?.tier!=null||t.redactionTier!=null)){const l=t.redaction?.tier!=null?t.redaction.tier:t.redactionTier,r=P(e,l);r!==l&&(t.redaction?.tier!=null?t.redaction={...t.redaction,tier:r}:t.redactionTier=r,o.push("redaction"))}return{settings:t,changed:o}}function D(e){const n=e?.org?.name||e?.org?.id;return n?`Managed by ${n}`:"Managed"}function j(e){return Array.isArray(e)?`[${e.map(j).join(",")}]`:e&&typeof e=="object"?`{${Object.keys(e).sort().map(n=>`${JSON.stringify(n)}:${j(e[n])}`).join(",")}}`:JSON.stringify(e)}async function H(e,{subtle:n=null}={}){const t=n||globalThis.crypto?.subtle;if(!t)throw new a("no WebCrypto available on this runtime");const o=new TextEncoder().encode(j(b(e))),i=await t.digest("SHA-256",o);return[...new Uint8Array(i)].map(l=>l.toString(16).padStart(2,"0")).join("")}async function K(e,{verify:n,...t}={}){if(typeof n!="function")throw new a("verifyPolicyToken needs a verify function (entitlement.js verifySignedToken)");const o=await n(e,{...t,typ:S});if(!o||!o.policy)return null;try{return b(o.policy)}catch{return null}}export{C as ACCESS_DELEGATION,k as FEATURES,O as LOCKABLE,y as PERMISSION_MODES,S as POLICY_TOKEN_TYPE,x as POLICY_VERSION,T as PRODUCT_HOSTS,a as PolicyError,d as REDACTION_TIERS,I as accessDelegation,L as agentAllowed,v as applyPolicyToSettings,B as egressAllowed,U as endpointAllowed,M as featureEnabled,m as globToRegExp,z as modelAllowed,b as normalizeOrgPolicy,N as permissionMode,H as policyHash,D as policyLabel,P as redactionTier,_ as routeAllowed,q as settingLocked,K as verifyPolicyToken};
1
+ import{SANDBOX_RUNTIMES as A,SANDBOX_READ_MODES as $,parseWrapperTemplate as R}from"./sandbox-choice.js";class l extends Error{constructor(r){super(r),this.name="PolicyError"}}const O=1,z="policy",w=Object.freeze(["off","basic","full"]),k=Object.freeze({off:0,basic:1,full:2}),E=Object.freeze(["gateway","endpoints","redaction","features","agents"]),S=Object.freeze(["webllm","meetings","channels","agents"]),m=Object.freeze(["plan","default","acceptEdits","bypassPermissions"]),U=Object.freeze({plan:0,default:1,acceptEdits:2,bypassPermissions:3}),L=Object.freeze(["api.chatpanel.net","dl.chatpanel.net","huggingface.co","registry.npmjs.org","api.github.com"]),f=e=>typeof e=="string"?e.trim():"",c=e=>Array.isArray(e)?e.map(f).filter(Boolean):[];function b(e){const r=f(e).toLowerCase().replace(/^https?:\/\//,"").replace(/\/.*$/,"");if(!r)return"";if(!/^(\*\.)?[a-z0-9.-]+(:\d{1,5})?$/.test(r)&&!/^\[[0-9a-f:]+\](:\d{1,5})?$/.test(r)&&r!=="*")throw new l(`invalid host pattern "${e}"`);return r}function y(e){if(e==null)return y({});if(typeof e!="object"||Array.isArray(e))throw new l("policy must be an object");const r=e.v==null?O:Number(e.v);if(r!==O)throw new l(`unsupported policy version ${e.v}`);const n={v:r};if(e.org!=null){const t=f(e.org.id);if(!t||!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(t))throw new l("org.id must be a short identifier");n.org={id:t,name:f(e.org.name)||t}}const o=(t,s)=>{if(t==null)return;if(typeof t!="object")throw new l(`${s} must be an object`);const u={};return t.allow!=null&&(u.allow=c(t.allow).map(b).filter(Boolean)),t.deny!=null&&(u.deny=c(t.deny).map(b).filter(Boolean)),u},a=o(e.endpoints,"endpoints");a&&(n.endpoints=a);const i=o(e.egress,"egress");if(i&&(n.egress=i),e.models!=null){if(typeof e.models!="object")throw new l("models must be an object");n.models={},e.models.allow!=null&&(n.models.allow=c(e.models.allow)),e.models.deny!=null&&(n.models.deny=c(e.models.deny))}if(e.agents!=null){if(typeof e.agents!="object")throw new l("agents must be an object");const t={};if(e.agents.allow!=null&&(t.allow=c(e.agents.allow).map(s=>s.toLowerCase())),e.agents.deny!=null&&(t.deny=c(e.agents.deny).map(s=>s.toLowerCase())),e.agents.permissionModeCap!=null){const s=f(e.agents.permissionModeCap);if(!m.includes(s))throw new l(`agents.permissionModeCap must be one of ${m.join(", ")}`);t.permissionModeCap=s}n.agents=t}if(e.routing!=null){if(typeof e.routing!="object")throw new l("routing must be an object");const t=Array.isArray(e.routing.rules)?e.routing.rules:[];n.routing={rules:t.map((s,u)=>{if(!s||typeof s!="object")throw new l(`routing.rules[${u}] must be an object`);const p=c(s.models),j=c(s.endpoints).map(b).filter(Boolean);if(!p.length)throw new l(`routing.rules[${u}] needs at least one model pattern`);if(!j.length)throw new l(`routing.rules[${u}] needs at least one endpoint`);return{models:p,endpoints:j}})}}if(e.redaction!=null){if(typeof e.redaction!="object")throw new l("redaction must be an object");const t={};if(e.redaction.floor!=null){const s=f(e.redaction.floor).toLowerCase();if(!w.includes(s))throw new l(`redaction.floor must be one of ${w.join(", ")}`);t.floor=s}e.redaction.dictionary!=null&&(t.dictionary=c(e.redaction.dictionary)),e.redaction.requireBeforeCloud!=null&&(t.requireBeforeCloud=!!e.redaction.requireBeforeCloud),n.redaction=t}if(e.client!=null){if(typeof e.client!="object")throw new l("client must be an object");const t={};if(e.client.gatewayUrl!=null){const s=f(e.client.gatewayUrl);if(s&&!/^https?:\/\/[^\s/]+(\/.*)?$/.test(s))throw new l("client.gatewayUrl must be an http(s) URL");t.gatewayUrl=s.replace(/\/+$/,"")}if(e.client.managedRequired!=null&&(t.managedRequired=!!e.client.managedRequired),e.client.lock!=null){const s=c(e.client.lock);for(const u of s)if(!E.includes(u))throw new l(`client.lock: unknown setting "${u}" (one of ${E.join(", ")})`);t.lock=[...new Set(s)]}if(e.client.features!=null){if(typeof e.client.features!="object")throw new l("client.features must be an object");t.features={};for(const[s,u]of Object.entries(e.client.features)){if(!S.includes(s))throw new l(`client.features: unknown feature "${s}" (one of ${S.join(", ")})`);t.features[s]=!!u}}n.client=t}if(e.sandbox!=null){if(typeof e.sandbox!="object"||Array.isArray(e.sandbox))throw new l("sandbox must be an object");const t={};if(e.sandbox.required!=null&&(t.required=!!e.sandbox.required),e.sandbox.runtime!=null){const s=f(e.sandbox.runtime);if(!A.includes(s))throw new l(`sandbox.runtime must be one of ${A.join(", ")}`);t.runtime=s}if(e.sandbox.command!=null)try{t.command=R(e.sandbox.command)}catch(s){throw new l(s.message)}if(e.sandbox.image!=null){const s=f(e.sandbox.image);if(!/^[A-Za-z0-9][A-Za-z0-9._\/:@-]{0,254}$/.test(s))throw new l("sandbox.image must be an image reference");t.image=s}if(e.sandbox.reads!=null){const s=f(e.sandbox.reads);if(!$.includes(s))throw new l(`sandbox.reads must be one of ${$.join(", ")}`);t.reads=s}if(e.sandbox.mode!=null){const s=f(e.sandbox.mode);if(!["open","allowlist"].includes(s))throw new l("sandbox.mode must be open or allowlist (an org that sandboxes agents itself sets runtime: managed)");t.mode=s}if(t.runtime==="command"&&!t.command)throw new l("sandbox.runtime command needs sandbox.command");if(t.runtime==="container"&&!t.image)throw new l("sandbox.runtime container needs sandbox.image");Object.keys(t).length&&(n.sandbox=t)}if(e.gateway!=null){if(typeof e.gateway!="object"||Array.isArray(e.gateway))throw new l("gateway must be an object");e.gateway.confined!=null&&(n.gateway={confined:!!e.gateway.confined})}if(e.access!=null){if(typeof e.access!="object"||Array.isArray(e.access))throw new l("access must be an object");const t={};if(e.access.delegation!=null){const s=f(e.access.delegation);if(!C.includes(s))throw new l(`access.delegation must be one of ${C.join(", ")}`);t.delegation=s}for(const s of["deny","allow"]){if(e.access[s]==null)continue;const u=c(e.access[s]).map(p=>String(p).trim()).filter(Boolean);for(const p of u)if(p.length>1024||p.includes("\0"))throw new l(`access.${s}: not a path`);t[s]=u}Object.keys(t).length&&(n.access=t)}return n}function I(e={},r=null){const n=r?.sandbox||{},o={...e},a=[];for(const i of["runtime","command","image","reads","mode"])n[i]!==void 0&&(o[i]=n[i],a.push(i));return n.required!==void 0&&(o.required=n.required?"policy":!1,a.push("required")),{sandbox:o,locked:a}}const C=Object.freeze(["allowed","never"]);function D(e){return e?.access?.delegation==="never"?"never":"allowed"}function d(e){const r=f(e).toLowerCase();if(!r)return{host:"",port:""};try{const n=new URL(/^[a-z][a-z0-9+.-]*:\/\//.test(r)?r:`http://${r}`);return{host:n.hostname.replace(/^\[|\]$/g,""),port:n.port||(n.protocol==="https:"?"443":n.protocol==="http:"?"80":"")}}catch{return{host:r.split(":")[0],port:""}}}function P(e,r,n){if(e==="*")return!0;const[o,a]=e.startsWith("[")?[e.replace(/^\[([^\]]+)\].*$/,"$1"),e.replace(/^\[[^\]]+\]:?/,"")]:(()=>{const i=e.lastIndexOf(":");return i>0?[e.slice(0,i),e.slice(i+1)]:[e,""]})();if(a&&a!==n)return!1;if(o.startsWith("*.")){const i=o.slice(1);return r.endsWith(i)&&r.length>i.length}return r===o}function g(e,r,n){return(e||[]).some(o=>P(o,r,n))}function h(e){return new RegExp(`^${String(e).split("*").map(r=>r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join(".*")}$`,"i")}function T(e,r){const n=e?.endpoints;if(!n)return!0;const{host:o,port:a}=d(r);return!o||g(n.deny,o,a)?!1:n.allow&&n.allow.length?g(n.allow,o,a):!0}function M(e,r){const n=e?.models;if(!n)return!0;const o=f(r);return(n.deny||[]).some(a=>h(a).test(o))?!1:n.allow&&n.allow.length?n.allow.some(a=>h(a).test(o)):!0}function _(e,r){const n=e?.agents;if(!n)return!0;const o=f(r).toLowerCase();return(n.deny||[]).includes(o)?!1:n.allow&&n.allow.length?n.allow.includes(o):!0}function v(e,r){const n=e?.agents?.permissionModeCap,o=m.includes(r)?r:"default";return n?U[o]<=U[n]?o:n:o}function H(e,{model:r="",endpoint:n=""}={}){if(!e)return{ok:!0,reason:""};if(r&&!M(e,r))return{ok:!1,reason:`model "${r}" is not allowed by your organisation's policy`};if(n&&!T(e,n))return{ok:!1,reason:`endpoint ${d(n).host} is not allowed by your organisation's policy`};const o=e.routing?.rules||[];if(r&&n&&o.length){const a=o.find(i=>i.models.some(t=>h(t).test(r)));if(a){const{host:i,port:t}=d(n);if(!g(a.endpoints,i,t))return{ok:!1,reason:`model "${r}" may only be sent to ${a.endpoints.join(", ")} under your organisation's policy`}}}return{ok:!0,reason:""}}function q(e,r){const n=e?.redaction?.floor,o=w.includes(r)?r:"basic";return n?k[o]>=k[n]?o:n:o}function W(e,r,{local:n=!1}={}){const o=e?.egress;if(!o)return!0;const{host:a,port:i}=d(r);return!a||g(o.deny,a,i)?!1:n||L.includes(a)||e.endpoints?.allow?.length&&g(e.endpoints.allow,a,i)&&!g(e.endpoints?.deny,a,i)?!0:g(o.allow,a,i)}function K(e,r){return!!e?.client?.lock?.includes(r)}function N(e,r){const n=e?.client?.features;return!n||n[r]==null?!0:!!n[r]}function Y(e,r){const n={...r||{}},o=[];if(!e)return{settings:n,changed:o};const a=e.client||{};if(a.gatewayUrl&&n.gatewayUrl!==a.gatewayUrl&&(n.gatewayUrl=a.gatewayUrl,o.push("gatewayUrl")),Array.isArray(n.endpoints)&&e.endpoints){const i=n.endpoints.filter(t=>!t||typeof t!="object"?!1:!t.baseUrl||a.gatewayUrl&&d(t.baseUrl).host===d(a.gatewayUrl).host&&d(t.baseUrl).port===d(a.gatewayUrl).port?!0:T(e,t.baseUrl));i.length!==n.endpoints.length&&(n.endpoints=i,o.push("endpoints"))}if(Array.isArray(n.endpoints)&&e.agents){const i=n.endpoints.filter(t=>t?.kind!=="bridge"||_(e,t.bridgeAgent||t.agent||t.id));i.length!==n.endpoints.length&&(n.endpoints=i,o.push("agents"))}if(Array.isArray(n.endpoints)&&!N(e,"webllm")){const i=n.endpoints.filter(t=>t?.kind!=="webllm");i.length!==n.endpoints.length&&(n.endpoints=i,o.push("webllm"))}if(e.redaction?.floor&&(n.redaction?.tier!=null||n.redactionTier!=null)){const i=n.redaction?.tier!=null?n.redaction.tier:n.redactionTier,t=q(e,i);t!==i&&(n.redaction?.tier!=null?n.redaction={...n.redaction,tier:t}:n.redactionTier=t,o.push("redaction"))}return{settings:n,changed:o}}function J(e){const r=e?.org?.name||e?.org?.id;return r?`Managed by ${r}`:"Managed"}function x(e){return Array.isArray(e)?`[${e.map(x).join(",")}]`:e&&typeof e=="object"?`{${Object.keys(e).sort().map(r=>`${JSON.stringify(r)}:${x(e[r])}`).join(",")}}`:JSON.stringify(e)}async function X(e,{subtle:r=null}={}){const n=r||globalThis.crypto?.subtle;if(!n)throw new l("no WebCrypto available on this runtime");const o=new TextEncoder().encode(x(y(e))),a=await n.digest("SHA-256",o);return[...new Uint8Array(a)].map(i=>i.toString(16).padStart(2,"0")).join("")}async function Z(e,{verify:r,...n}={}){if(typeof r!="function")throw new l("verifyPolicyToken needs a verify function (entitlement.js verifySignedToken)");const o=await r(e,{...n,typ:z});if(!o||!o.policy)return null;try{return y(o.policy)}catch{return null}}export{C as ACCESS_DELEGATION,S as FEATURES,E as LOCKABLE,m as PERMISSION_MODES,z as POLICY_TOKEN_TYPE,O as POLICY_VERSION,L as PRODUCT_HOSTS,l as PolicyError,w as REDACTION_TIERS,D as accessDelegation,_ as agentAllowed,Y as applyPolicyToSettings,W as egressAllowed,T as endpointAllowed,N as featureEnabled,h as globToRegExp,M as modelAllowed,y as normalizeOrgPolicy,v as permissionMode,X as policyHash,J as policyLabel,q as redactionTier,H as routeAllowed,I as sandboxUnderPolicy,K as settingLocked,Z as verifyPolicyToken};
@@ -0,0 +1 @@
1
+ function r({bunMain:t="",versions:e={},execPath:i=""}={}){if(e.electron)return!1;const n=String(t||"");if(n)return n.startsWith("/$bunfs/")||/^[A-Za-z]:[\\/]~BUN[\\/]/.test(n);const s=String(i||"").split(/[\\/]/).pop().toLowerCase();return e.bun?!1:!(s.startsWith("node")||s.startsWith("bun"))}function o(){const t=globalThis.process;return{bunMain:typeof globalThis.Bun<"u"?String(globalThis.Bun.main||""):"",versions:t?.versions||{},execPath:t?.execPath||""}}export{o as hostFacts,r as isCompiledBinary};
@@ -0,0 +1 @@
1
+ const r=(o,e)=>{if(!o)throw Object.assign(new Error(e),{code:"SHAPE"})},c=Object.freeze(["enumerated","inverted"]),h=Object.freeze(["srt","container","command","managed"]);function u(o){const e=Array.isArray(o)?null:String(o??"").trim();let n;if(Array.isArray(o))n=o.map(String);else if(e.startsWith("[")){try{n=JSON.parse(e)}catch{r(!1,"sandbox command: not a JSON array")}r(Array.isArray(n)&&n.every(t=>typeof t=="string"),"sandbox command: a JSON array of strings")}else n=e.split(/\s+/).filter(Boolean);return r(n.length>0&&n.length<=128,"sandbox command: 1–128 words"),r(n.every(t=>t.length<=1024&&!t.includes("\0")),"sandbox command: a word too long or with a NUL"),r(!n[0].includes("{"),"sandbox command: the first word is the wrapper program, not a placeholder"),r(n.filter(t=>t==="{cmd}").length===1,"sandbox command: exactly one {cmd} word, where the command goes"),n}function m(o,{bin:e,argv:n=[],workdir:t="",home:i="",hosts:d=[],mode:a=""}={}){const l=s=>s.replaceAll("{workdir}",t).replaceAll("{home}",i).replaceAll("{hosts}",d.join(",")).replaceAll("{mode}",a);return o.flatMap(s=>s==="{cmd}"?[e,...n]:[l(s)])}function p(o){if(!o?.ok)return{cls:"off",text:"The local runtime (bridge) is not running — nothing is sandboxed because nothing is running."};const e=o.sandbox;if(!e)return{cls:"warn",text:`This bridge (v${o.version||"?"}) predates the sandbox — update the gateway.`};const n=e.required==="policy"?" Your organisation requires it: anything it cannot be applied to is refused, and nothing runs outside it.":e.required?" Required: anything it cannot be applied to is refused rather than run as you.":"";if(e.enabled===!1)return e.required?{cls:"bad",text:`The sandbox is required but switched off, so nothing runs.${n}`}:{cls:"warn",text:"Sandbox off — agents, MCP servers and workers run as you, with your files and your network."};if(!e.runtime){const a=`${e.reason||"no runtime"}${e.install?` (${e.install})`:""}`;return e.required?{cls:"bad",text:`Nothing runs — the sandbox is required and cannot be applied here: ${a}.`}:{cls:"warn",text:`Not sandboxed — ${a}`}}const t=String(e.runtime);if(t==="managed")return{cls:"ok",text:`Managed by your organisation — your agents run inside its sandbox, so ChatPanel does not add one of its own.${n}`};if(t.startsWith("command:"))return{cls:"ok",text:`Sandboxed by your own command (${t.slice(8)}) — it decides what each process may touch; ChatPanel hands it the working folder and the hosts each one is allowed.${n}`};if(t.startsWith("container:"))return{cls:"ok",text:`Sandboxed in containers (${t.slice(10)}) — each process sees only its working folder, with no capabilities${e.mode==="none"?" and no network":""}.${n}`};const i=e.mode==="allowlist"?"only its own service, the developer hosts and what you allow":e.mode==="none"?"nothing":"any public host through the sandbox proxy — never your machine, your network or a cloud metadata address",d=e.mode==="allowlist"?e.ownSessions?" A process granted hosts of its own gets a session of its own.":" Hosts granted on a card need a session of their own, which is not available here, so they share the allow list.":"";return{cls:"ok",text:`Sandboxed (${t}, ${e.platform||""}): the working directory writable, your secrets unreadable, the network ${i}.${d}${n}`}}export{c as SANDBOX_READ_MODES,h as SANDBOX_RUNTIMES,m as expandWrapper,u as parseWrapperTemplate,p as sandboxStatus};
package/sandbox-policy.js CHANGED
@@ -1 +1 @@
1
- import{EventError as L}from"./event.js";import{normalizeGrant as y,SIGN_IN_NAMES as P,SIGN_IN_LABELS as x}from"./sandbox-grant.js";import{SECRET_DIR_NAMES as T,SECRET_DIR_PAIRS as H}from"./secret-names.js";const $=Object.freeze(["none","process","container","kubernetes"]),N=Object.freeze(["open","allowlist","none","gateway"]),S=Object.freeze(["~/.chatpanel","~/.ssh","~/.gnupg","~/.aws","~/.azure","~/.config/gcloud","~/.kube","~/.netrc","~/.docker/config.json","~/.npmrc","~/.pypirc"]),p=e=>{let t=String(e||"").trim();for(;t.length>1&&(t.endsWith("/")||t.endsWith("\\"));)t=t.slice(0,-1);return t},O=(e,t)=>t&&(e==="~"||e.startsWith("~/")||e.startsWith("~\\"))?t+e.slice(1):e,m=e=>String(e||"").replace(/\\/g,"/"),A=(e,t)=>{const n=m(p(e)),r=m(p(t));return!!n&&!!r&&(n===r||n.startsWith(`${r}/`))},G=S.map(e=>m(p(e)).replace(/^~\//,"").split("/").filter(Boolean));function C(e,t=""){const n=m(p(O(p(e),t)));if(!n)return!1;for(const o of S)if(A(n,o)||t&&A(n,O(o,t)))return!0;const r=n.split("/").filter(Boolean).map(o=>o.toLowerCase());return G.some(o=>{for(let s=0;s+o.length<=r.length;s+=1)if(o.every((l,c)=>l.toLowerCase()===r[s+c]))return!0;return!1})}const z=Object.freeze([...[...T].map(e=>`~/**/${e}`),...H.map(([e,t])=>`~/**/${e}/${t}`),...[".env",".netrc","_netrc",".git-credentials",".pgpass",".my.cnf",".npmrc",".pypirc",".htpasswd",".terraformrc",".vault-token","credentials","credentials.toml","id_rsa","id_dsa","id_ecdsa","id_ed25519","*.pem","*.p12","*.pfx","*.jks","*.keystore","*.kdbx","*.ppk"].map(e=>`~/**/${e}`)]);function se(e,{home:t="",globs:n=[...S,...z]}={}){const r=m(p(O(p(e),t)));if(!r)return null;const o=r.split("/").filter(Boolean),s=m(p(t));for(const l of n){const c=m(p(l));if(c){if(c.startsWith("~/**/")){if(s&&!A(r,s))continue;const a=c.slice(5).split("/").filter(Boolean).map(K);for(let i=0;i+a.length<=o.length;i+=1)if(a.every((d,g)=>d.test(o[i+g])))return l;continue}if(A(r,O(c,t)))return l}}return null}function K(e){return new RegExp(`^${e.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,"[^/]*")}$`,"i")}const D=".cache/chatpanel/ca-bundle.crt",ie=".cache/chatpanel/ca-bundle.pem";function ae(e=""){const t=m(p(e));return t?`${t}/${D}`:`~/${D}`}const U=Object.freeze(["~/.npm","~/.cache","~/.local","~/Library/Caches","~/.nvm","~/.volta","~/.asdf","~/.nodenv","~/.bun","~/.deno","~/.pyenv","~/.rye","~/.pdm","~/.cargo","~/.rustup","~/go","~/.goenv","~/.m2","~/.gradle","~/.sdkman","~/.jenv","~/.gem","~/.rbenv","~/.bundle","~/.composer","~/.nuget","~/.dotnet","~/.stack","~/.ghcup","~/.opam","~/.pub-cache","~/.gitconfig","~/.config/git"]),W=Object.freeze(["/tmp","/private/tmp","~/.cache","~/.npm","~/.local/share/uv","~/.local/state"]),j=Object.freeze({codex:Object.freeze({hosts:["api.openai.com","chatgpt.com","auth.openai.com","*.openai.com"],write:["~/.codex"]}),claude:Object.freeze({hosts:["api.anthropic.com","*.anthropic.com","claude.ai","statsig.anthropic.com"],write:["~/.claude","~/.claude.json"],keychain:!0,sockets:["/tmp/cc-socks","/private/tmp/cc-socks"]}),antigravity:Object.freeze({hosts:["*.googleapis.com","*.google.com","oauth2.googleapis.com"],write:["~/.gemini","~/.antigravity","~/.config/antigravity"]}),custom:Object.freeze({hosts:[],write:[]}),mcp:Object.freeze({hosts:[],write:[]}),extract:Object.freeze({hosts:[],write:[]}),"model-server":Object.freeze({hosts:["huggingface.co","*.huggingface.co","*.hf.co","cdn-lfs.huggingface.co","cas-bridge.xethub.hf.co","dl.chatpanel.net"],write:[]})}),Y="~/Library/Keychains",B=/(^|\/)Keychains\/?$/,R=Object.freeze({aws:Object.freeze({label:x.aws,where:"~/.aws",read:["~/.aws","~/.aws/credentials","~/.aws/sso"],hosts:["*.amazonaws.com","*.api.aws"],failure:/could not load (aws )?credentials|unable to locate credentials|CredentialsProviderError|NoCredentialProviders|failed to refresh cached credentials|aws credentials? (not found|could not be)/i}),oci:Object.freeze({label:x.oci,where:"~/.oci",read:["~/.oci","~/.oci/**/*.pem"],hosts:["*.oraclecloud.com"],failure:/could not find config file at .*\.oci|ConfigFileNotFound|oci config(uration)? (file )?(was )?not found|\.oci[\\/]config.*(not found|no such file|not permitted|permission denied)/i}),gcloud:Object.freeze({label:x.gcloud,where:"~/.config/gcloud",read:["~/.config/gcloud"],hosts:["*.googleapis.com","accounts.google.com"],failure:/could not load the default credentials|DefaultCredentialsError|could not (automatically )?(find|determine) (default )?credentials|application default credentials (were|are) not (found|available)/i}),azure:Object.freeze({label:x.azure,where:"~/.azure",read:["~/.azure"],hosts:["*.azure.com","login.microsoftonline.com"],failure:/DefaultAzureCredential failed|CredentialUnavailableError|please run 'az login'|AzureCliCredential authentication (unavailable|failed)/i})});function k(e){return f((y(e).signIn||[]).flatMap(t=>R[t].read))}function M(e){return f((y(e).signIn||[]).flatMap(t=>R[t].hosts))}function ce(e,{grant:t=null,sandboxed:n=!0,agentLabel:r="This agent",agent:o=""}={}){if(!n)return null;let s=[];try{s=y(t).signIn||[]}catch{}const l=String(e||"");for(const c of P){const a=R[c];if(!(s.includes(c)||!a.failure.test(l)))return{store:c,line:`${r} could not sign in to ${a.label}: the ChatPanel sandbox keeps your ${a.label} sign-in (${a.where}) from every agent until you allow it. To allow it for this agent only, turn on ${a.label} under "May sign in with" in its Coding Agents settings${o?`, or run: chatpanel-gateway sandbox allow ${o} ${c}`:""}. Nothing else it is denied changes.`}}return null}const F=Object.freeze(["github.com","*.github.com","*.githubusercontent.com","gitlab.com","bitbucket.org","registry.npmjs.org","pypi.org","files.pythonhosted.org","crates.io","static.crates.io","proxy.golang.org","sum.golang.org","huggingface.co","*.huggingface.co"]),E=e=>!!e&&typeof e=="object"&&!Array.isArray(e),_=e=>Array.isArray(e)&&e.every(t=>typeof t=="string"&&t.length>0),u=(e,t)=>{if(!e)throw new L("SHAPE",t)},f=e=>[...new Set(e)];function v(e){u(E(e),"kit: must be an object"),u(typeof e.id=="string"&&e.id.length>0,"kit: id required"),u(E(e.network)&&N.includes(e.network.mode),`kit ${e.id}: network.mode must be one of ${N.join(", ")}`),u(_(e.network.allowedDomains||[]),`kit ${e.id}: network.allowedDomains must be string[]`),u(_(e.network.deniedDomains||[]),`kit ${e.id}: network.deniedDomains must be string[]`),u(E(e.filesystem),`kit ${e.id}: filesystem required`);for(const t of["allowWrite","denyWrite","allowRead","denyRead"])u(_(e.filesystem[t]||[]),`kit ${e.id}: filesystem.${t} must be string[]`);return u(_(e.filesystem.allowWrite)&&e.filesystem.allowWrite.length>0,`kit ${e.id}: filesystem.allowWrite must name at least the working directory`),u(_(e.network.unixSockets||[])&&(e.network.unixSockets||[]).every(t=>t.startsWith("/")&&t.replace(/\/+$/,"")!==""),`kit ${e.id}: network.unixSockets must be absolute folders, never /`),e.image!=null&&u(typeof e.image=="string",`kit ${e.id}: image must be a string`),e.credentials!=null&&u(Array.isArray(e.credentials)&&e.credentials.every(t=>E(t)&&typeof t.name=="string"&&typeof t.forHost=="string"),`kit ${e.id}: credentials must be [{ name, forHost }]`),{id:e.id,kind:e.kind||"process",image:e.image||null,network:{mode:e.network.mode,allowedDomains:f(e.network.allowedDomains||[]),deniedDomains:f(e.network.deniedDomains||[]),...(e.network.unixSockets||[]).length?{unixSockets:f(e.network.unixSockets)}:{}},filesystem:{allowWrite:f(e.filesystem.allowWrite),denyWrite:f(e.filesystem.denyWrite||[]),allowRead:f(e.filesystem.allowRead||[]),denyRead:f(e.filesystem.denyRead||[])},credentials:e.credentials||[],limits:E(e.limits)?{...e.limits}:{}}}import{normalizeGrant as _e,mergeGrants as be,mergePersonGrants as xe,parseHostList as Se,SIGN_IN_NAMES as Oe,SIGN_IN_LABELS as Ae}from"./sandbox-grant.js";const q=e=>String(e).replace(/([\\/])\*$/,"$1**");function le({engine:e="custom",cwd:t,mode:n="open",extraHosts:r=[],extraWrite:o=[],extraDenyRead:s=[],grant:l=null,home:c="",reads:a="enumerated",readOnly:i=!1,peers:d=!0}={}){const g=j[e]||j.custom;u(typeof t=="string"&&t.length>0,"kit: cwd required");const h=y(l);return v({id:`${e}:${n}`,kind:"process",network:{mode:n,allowedDomains:n==="open"?["*"]:n==="none"?[]:f([...g.hosts,...F,...r,...h.hosts,...M(h)]),deniedDomains:[],unixSockets:d?[...g.sockets||[]]:[]},filesystem:{allowWrite:f(i?[...W,...g.write]:[t,...W,...g.write,...[...o,...h.write].filter(I=>!C(I,c)).map(q)]),denyWrite:[],allowRead:f(a==="inverted"?[t,...U,...g.write,...g.keychain?[Y]:[],...V({grant:h,extraWrite:o,cwd:t,home:c}),...k(h)]:[t,...k(h)]),denyRead:f([...a==="inverted"?["~",...c?[c]:[]]:[],...[...S,...z].filter(I=>!(g.keychain&&B.test(I))),...s])}})}function X(e){if(!/[*?[{]/.test(e))return e;const t=e.split(/[\\/]/),n=t.findIndex(s=>/[*?[{]/.test(s)),r=e.includes("\\")&&!e.includes("/")?"\\":"/",o=t.slice(0,n).join(r);return o&&o!=="~"?o:""}function V({grant:e=null,extraWrite:t=[],cwd:n="",home:r=""}={}){const o=i=>r&&(i==="~"||i.startsWith("~/")||i.startsWith("~\\"))?r+i.slice(1):i,s=i=>{let d=String(i||"").trim();for(;d.length>1&&(d.endsWith("/")||d.endsWith("\\"));)d=d.slice(0,-1);return d},l=(i,d)=>i===d||i.startsWith(`${d}/`)||i.startsWith(`${d}\\`),c=s(o(n)),a=[];for(const i of[...y(e).write,...Array.isArray(t)?t:[]]){const d=X(s(o(s(i))));if(d&&!C(d,r)&&!(c&&l(d,c))&&!a.some(g=>l(d,g))){for(let g=a.length-1;g>=0;g--)l(a[g],d)&&a.splice(g,1);a.push(d)}}return a}function de(e,{loopback:t=[],home:n="",platform:r="darwin"}={}){const o=v(e),s=c=>n&&c.startsWith("~")?n+c.slice(1):c,l=o.network.mode==="none"?[]:t.map(c=>`127.0.0.1:${c}`);return{network:{allowedDomains:f([...o.network.allowedDomains,...l]),deniedDomains:o.network.deniedDomains,allowLocalBinding:r==="darwin"&&l.length>0,...r==="darwin"&&o.network.unixSockets?.length?{allowUnixSockets:o.network.unixSockets}:{}},filesystem:{denyRead:o.filesystem.denyRead.map(s),allowRead:o.filesystem.allowRead.map(s),allowWrite:o.filesystem.allowWrite.map(s),denyWrite:o.filesystem.denyWrite.map(s)}}}function ue({layer:e="none",runtime:t=null,kitId:n=null,mode:r=null,reason:o=null,hosts:s=null}={}){return u($.includes(e),`sandbox record: layer must be one of ${$.join(", ")}`),{layer:e,runtime:t,kit:n,mode:r,...e==="none"&&o?{reason:o}:{},...Array.isArray(s)&&s.length?{hosts:f(s)}:{}}}function Q(e,{platform:t="darwin"}={}){const n=String(e);if(t==="win32"){u(!/[\r\n]/.test(n),"win32 command line: an argument holds a newline, which cmd.exe cannot carry");let r=n;if(n===""||/[ \t\v"]/.test(n)){r='"';for(let o=0;o<n.length;o++){let s=0;for(;o<n.length&&n[o]==="\\";)s++,o++;if(o===n.length){r+="\\".repeat(s*2);break}n[o]==='"'?r+="\\".repeat(s*2+1)+'"':r+="\\".repeat(s)+n[o]}r+='"'}return r.replace(/[()%!^"<>&|]/g,"^$&")}return n===""?"''":/^[A-Za-z0-9_\-./=:@%+,]+$/.test(n)?n:`'${n.replace(/'/g,"'\\''")}'`}const Z=Object.freeze(["SSL_CERT_FILE","REQUESTS_CA_BUNDLE","CURL_CA_BUNDLE","GIT_SSL_CAINFO"]);function fe(e={},{platform:t="darwin",caBundle:n=""}={}){const r={NODE_USE_ENV_PROXY:"1"};if(n)for(const o of Z)e[o]||(r[o]=n);if(t==="linux")for(const o of["no_proxy","NO_PROXY"])typeof e[o]=="string"&&(r[o]=e[o].split(",").map(s=>s.trim()).filter(s=>s&&!/^(localhost|127\.0\.0\.1|::1|\[::1\])$/i.test(s)).join(","));return r}function ge(e,t=[],{platform:n="darwin"}={}){return[e,...t].map(r=>Q(r,{platform:n})).join(" ")}const pe=/[\\/]+/,w=e=>String(e||"").replace(/\//g,"\\").replace(/\\+$/,"").toLowerCase(),J=e=>/^[a-z]:$/i.test(w(e))||w(e)==="";function b(e,t){const n=w(e),r=w(t);return!!r&&n!==r&&n.startsWith(r+"\\")}function ee(e){const t=String(e||"").replace(/\//g,"\\").split("\\");if(t.length<2)return"";const n=t.findIndex(r=>r.toLowerCase()==="node_modules");return(n>0?t.slice(0,n):t.slice(0,-1)).join("\\")}function me({kit:e,files:t=[],home:n}){u(typeof n=="string"&&n.length>0,"windows grants: home required");const r=a=>(a.startsWith("~")?n+a.slice(1):a).replace(/\//g,"\\"),o=[];for(const a of e?.filesystem?.allowWrite||[]){const i=r(a);!/^[a-z]:\\/i.test(i)&&!i.startsWith("\\\\")||(u(!J(i)&&w(i)!==w(n),`windows grants: a write on ${i} would hand the sandbox account the whole profile`),b(i,n)&&o.push(i))}const s=(a,i)=>i.some(d=>w(a)===w(d)||b(a,d)),l=[];for(const a of t){const i=ee(a);i&&b(i,n)&&l.push(i)}for(const a of e?.filesystem?.allowRead||[]){if(/[*?[]/.test(a))continue;const i=r(a);b(i,n)&&l.push(i)}const c=f(l).filter(a=>!s(a,o));return{read:c.filter(a=>!c.some(i=>i!==a&&b(a,i))),write:f(o)}}const te=Object.freeze(["ANTHROPIC_API_KEY","ANTHROPIC_AUTH_TOKEN","ANTHROPIC_BASE_URL","CLAUDE_CONFIG_DIR","OPENAI_API_KEY","OPENAI_BASE_URL","OPENAI_API_BASE","CODEX_HOME","GEMINI_API_KEY","GOOGLE_API_KEY","DEEPSEEK_API_KEY","DSH_HOME","OPENROUTER_API_KEY","GROQ_API_KEY","XAI_API_KEY","MISTRAL_API_KEY"]);function we({engine:e="custom",home:t,env:n={},base:r={},carry:o=te}={}){u(typeof t=="string"&&t.length>0,"windows env: home required");const s=t.replace(/\//g,"\\"),l=/^[a-z]:/i.test(s)?s.slice(0,2):"",c={USERPROFILE:s,...l?{HOMEDRIVE:l,HOMEPATH:s.slice(2)}:{}};for(const[a,i]of Object.entries(n))typeof i=="string"&&(r[a]!==i||o.includes(a))&&(c[a]=i);return e==="codex"&&!c.CODEX_HOME&&(c.CODEX_HOME=s+"\\.codex"),c}function he(e,t){u(Array.isArray(e),"srt-win argv: must be an array");const n=e.indexOf("exec"),r=n>=0?e.indexOf("--",n+1):-1;u(n>=0&&r>n,"srt-win argv: expected `exec … -- <shell> <command>`");const o=new Set;for(let l=n+1;l<r;l++)e[l]==="--env"&&typeof e[l+1]=="string"&&o.add(e[l+1].split("=")[0].toUpperCase());const s=[];for(const[l,c]of Object.entries(t||{}))u(/^[A-Za-z_][A-Za-z0-9_]*$/.test(l),`srt-win env: bad key ${l}`),u(!/[\r\n]/.test(c),`srt-win env: ${l} holds a newline`),o.has(l.toUpperCase())||s.push("--env",`${l}=${c}`);return[...e.slice(0,r),...s,...e.slice(r)]}export{D as CA_BUNDLE_RELPATH,ie as CA_BUNDLE_RELPATH_LEGACY,W as COMMON_WRITE,F as DEVELOPER_HOSTS,j as ENGINE_KITS,N as NETWORK_MODES,$ as SANDBOX_LAYERS,S as SECRET_PATHS,z as SECRET_READ_GLOBS,Ae as SIGN_IN_LABELS,Oe as SIGN_IN_NAMES,R as SIGN_IN_STORES,U as TOOLCHAIN_READ,te as WINDOWS_CARRY_KEYS,ae as caBundlePath,C as isSecretWritePath,le as kitFor,be as mergeGrants,xe as mergePersonGrants,_e as normalizeGrant,Se as parseHostList,se as readDenialFor,fe as sandboxEnvOverlay,ue as sandboxRecord,ge as shellJoin,Q as shellQuote,ce as signInDenial,M as signInHosts,k as signInRead,de as toSrtSettings,b as underWindowsHome,v as validateKit,me as windowsGrantsFor,ee as windowsInstallRoot,we as windowsSandboxEnv,he as withSrtWinEnv,V as writableRoots};
1
+ import{EventError as L}from"./event.js";import{normalizeGrant as y,SIGN_IN_NAMES as P,SIGN_IN_LABELS as x}from"./sandbox-grant.js";import{SECRET_DIR_NAMES as T,SECRET_DIR_PAIRS as H}from"./secret-names.js";const k=Object.freeze(["none","process","container","kubernetes","external"]),N=Object.freeze(["open","allowlist","none","gateway"]),S=Object.freeze(["~/.chatpanel","~/.ssh","~/.gnupg","~/.aws","~/.azure","~/.config/gcloud","~/.kube","~/.netrc","~/.docker/config.json","~/.npmrc","~/.pypirc"]),g=e=>{let t=String(e||"").trim();for(;t.length>1&&(t.endsWith("/")||t.endsWith("\\"));)t=t.slice(0,-1);return t},O=(e,t)=>t&&(e==="~"||e.startsWith("~/")||e.startsWith("~\\"))?t+e.slice(1):e,m=e=>String(e||"").replace(/\\/g,"/"),A=(e,t)=>{const n=m(g(e)),r=m(g(t));return!!n&&!!r&&(n===r||n.startsWith(`${r}/`))},G=S.map(e=>m(g(e)).replace(/^~\//,"").split("/").filter(Boolean));function W(e,t=""){const n=m(g(O(g(e),t)));if(!n)return!1;for(const o of S)if(A(n,o)||t&&A(n,O(o,t)))return!0;const r=n.split("/").filter(Boolean).map(o=>o.toLowerCase());return G.some(o=>{for(let s=0;s+o.length<=r.length;s+=1)if(o.every((l,c)=>l.toLowerCase()===r[s+c]))return!0;return!1})}const j=Object.freeze([...[...T].map(e=>`~/**/${e}`),...H.map(([e,t])=>`~/**/${e}/${t}`),...[".env",".netrc","_netrc",".git-credentials",".pgpass",".my.cnf",".npmrc",".pypirc",".htpasswd",".terraformrc",".vault-token","credentials","credentials.toml","id_rsa","id_dsa","id_ecdsa","id_ed25519","*.pem","*.p12","*.pfx","*.jks","*.keystore","*.kdbx","*.ppk"].map(e=>`~/**/${e}`)]);function ae(e,{home:t="",globs:n=[...S,...j]}={}){const r=m(g(O(g(e),t)));if(!r)return null;const o=r.split("/").filter(Boolean),s=m(g(t));for(const l of n){const c=m(g(l));if(c){if(c.startsWith("~/**/")){if(s&&!A(r,s))continue;const a=c.slice(5).split("/").filter(Boolean).map(K);for(let i=0;i+a.length<=o.length;i+=1)if(a.every((d,u)=>d.test(o[i+u])))return l;continue}if(A(r,O(c,t)))return l}}return null}function K(e){return new RegExp(`^${e.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,"[^/]*")}$`,"i")}const C=".cache/chatpanel/ca-bundle.crt",ce=".cache/chatpanel/ca-bundle.pem";function le(e=""){const t=m(g(e));return t?`${t}/${C}`:`~/${C}`}const B=Object.freeze(["~/.npm","~/.cache","~/.local","~/Library/Caches","~/.nvm","~/.volta","~/.asdf","~/.nodenv","~/.bun","~/.deno","~/.pyenv","~/.rye","~/.pdm","~/.cargo","~/.rustup","~/go","~/.goenv","~/.m2","~/.gradle","~/.sdkman","~/.jenv","~/.gem","~/.rbenv","~/.bundle","~/.composer","~/.nuget","~/.dotnet","~/.stack","~/.ghcup","~/.opam","~/.pub-cache","~/.gitconfig","~/.config/git"]),z=Object.freeze(["/tmp","/private/tmp","~/.cache","~/.npm","~/.local/share/uv","~/.local/state"]),v=Object.freeze({codex:Object.freeze({hosts:["api.openai.com","chatgpt.com","auth.openai.com","*.openai.com"],write:["~/.codex"]}),claude:Object.freeze({hosts:["api.anthropic.com","*.anthropic.com","claude.ai","statsig.anthropic.com"],write:["~/.claude","~/.claude.json"],keychain:!0,sockets:["/tmp/cc-socks","/private/tmp/cc-socks"]}),antigravity:Object.freeze({hosts:["*.googleapis.com","*.google.com","oauth2.googleapis.com"],write:["~/.gemini","~/.antigravity","~/.config/antigravity"]}),custom:Object.freeze({hosts:[],write:[]}),mcp:Object.freeze({hosts:[],write:[]}),extract:Object.freeze({hosts:[],write:[]}),"model-server":Object.freeze({hosts:["huggingface.co","*.huggingface.co","*.hf.co","cdn-lfs.huggingface.co","cas-bridge.xethub.hf.co","dl.chatpanel.net"],write:[]})}),M="~/Library/Keychains",U=/(^|\/)Keychains\/?$/,I=Object.freeze({aws:Object.freeze({label:x.aws,where:"~/.aws",read:["~/.aws","~/.aws/credentials","~/.aws/sso"],hosts:["*.amazonaws.com","*.api.aws"],failure:/could not load (aws )?credentials|unable to locate credentials|CredentialsProviderError|NoCredentialProviders|failed to refresh cached credentials|aws credentials? (not found|could not be)/i}),oci:Object.freeze({label:x.oci,where:"~/.oci",read:["~/.oci","~/.oci/**/*.pem"],hosts:["*.oraclecloud.com"],failure:/could not find config file at .*\.oci|ConfigFileNotFound|oci config(uration)? (file )?(was )?not found|\.oci[\\/]config.*(not found|no such file|not permitted|permission denied)/i}),gcloud:Object.freeze({label:x.gcloud,where:"~/.config/gcloud",read:["~/.config/gcloud"],hosts:["*.googleapis.com","accounts.google.com"],failure:/could not load the default credentials|DefaultCredentialsError|could not (automatically )?(find|determine) (default )?credentials|application default credentials (were|are) not (found|available)/i}),azure:Object.freeze({label:x.azure,where:"~/.azure",read:["~/.azure"],hosts:["*.azure.com","login.microsoftonline.com"],failure:/DefaultAzureCredential failed|CredentialUnavailableError|please run 'az login'|AzureCliCredential authentication (unavailable|failed)/i})});function D(e){return f((y(e).signIn||[]).flatMap(t=>I[t].read))}function Y(e){return f((y(e).signIn||[]).flatMap(t=>I[t].hosts))}function de(e,{grant:t=null,sandboxed:n=!0,agentLabel:r="This agent",agent:o=""}={}){if(!n)return null;let s=[];try{s=y(t).signIn||[]}catch{}const l=String(e||"");for(const c of P){const a=I[c];if(!(s.includes(c)||!a.failure.test(l)))return{store:c,line:`${r} could not sign in to ${a.label}: the ChatPanel sandbox keeps your ${a.label} sign-in (${a.where}) from every agent until you allow it. To allow it for this agent only, turn on ${a.label} under "May sign in with" in its Coding Agents settings${o?`, or run: chatpanel-gateway sandbox allow ${o} ${c}`:""}. Nothing else it is denied changes.`}}return null}const F=Object.freeze(["github.com","*.github.com","*.githubusercontent.com","gitlab.com","bitbucket.org","registry.npmjs.org","pypi.org","files.pythonhosted.org","crates.io","static.crates.io","proxy.golang.org","sum.golang.org","huggingface.co","*.huggingface.co"]),E=e=>!!e&&typeof e=="object"&&!Array.isArray(e),_=e=>Array.isArray(e)&&e.every(t=>typeof t=="string"&&t.length>0),p=(e,t)=>{if(!e)throw new L("SHAPE",t)},f=e=>[...new Set(e)];function $(e){p(E(e),"kit: must be an object"),p(typeof e.id=="string"&&e.id.length>0,"kit: id required"),p(E(e.network)&&N.includes(e.network.mode),`kit ${e.id}: network.mode must be one of ${N.join(", ")}`),p(_(e.network.allowedDomains||[]),`kit ${e.id}: network.allowedDomains must be string[]`),p(_(e.network.deniedDomains||[]),`kit ${e.id}: network.deniedDomains must be string[]`),p(E(e.filesystem),`kit ${e.id}: filesystem required`);for(const t of["allowWrite","denyWrite","allowRead","denyRead"])p(_(e.filesystem[t]||[]),`kit ${e.id}: filesystem.${t} must be string[]`);return p(_(e.filesystem.allowWrite)&&e.filesystem.allowWrite.length>0,`kit ${e.id}: filesystem.allowWrite must name at least the working directory`),p(_(e.network.unixSockets||[])&&(e.network.unixSockets||[]).every(t=>t.startsWith("/")&&t.replace(/\/+$/,"")!==""),`kit ${e.id}: network.unixSockets must be absolute folders, never /`),e.image!=null&&p(typeof e.image=="string",`kit ${e.id}: image must be a string`),e.credentials!=null&&p(Array.isArray(e.credentials)&&e.credentials.every(t=>E(t)&&typeof t.name=="string"&&typeof t.forHost=="string"),`kit ${e.id}: credentials must be [{ name, forHost }]`),{id:e.id,kind:e.kind||"process",image:e.image||null,network:{mode:e.network.mode,allowedDomains:f(e.network.allowedDomains||[]),deniedDomains:f(e.network.deniedDomains||[]),...(e.network.unixSockets||[]).length?{unixSockets:f(e.network.unixSockets)}:{}},filesystem:{allowWrite:f(e.filesystem.allowWrite),denyWrite:f(e.filesystem.denyWrite||[]),allowRead:f(e.filesystem.allowRead||[]),denyRead:f(e.filesystem.denyRead||[])},credentials:e.credentials||[],limits:E(e.limits)?{...e.limits}:{}}}import{normalizeGrant as xe,mergeGrants as Se,mergePersonGrants as Oe,parseHostList as Ae,SIGN_IN_NAMES as Re,SIGN_IN_LABELS as Ie}from"./sandbox-grant.js";import{SANDBOX_READ_MODES as ke,SANDBOX_RUNTIMES as Ne,parseWrapperTemplate as We,expandWrapper as je}from"./sandbox-choice.js";const X=e=>String(e).replace(/([\\/])\*$/,"$1**");function q({engine:e="custom",cwd:t,mode:n="open",extraHosts:r=[],extraWrite:o=[],extraDenyRead:s=[],grant:l=null,home:c="",reads:a="enumerated",readOnly:i=!1,peers:d=!0}={}){const u=v[e]||v.custom;p(typeof t=="string"&&t.length>0,"kit: cwd required");const h=y(l);return $({id:`${e}:${n}`,kind:"process",network:{mode:n,allowedDomains:n==="open"?["*"]:n==="none"?[]:f([...u.hosts,...F,...r,...h.hosts,...Y(h)]),deniedDomains:[],unixSockets:d?[...u.sockets||[]]:[]},filesystem:{allowWrite:f(i?[...z,...u.write]:[t,...z,...u.write,...[...o,...h.write].filter(R=>!W(R,c)).map(X)]),denyWrite:[],allowRead:f(a==="inverted"?[t,...B,...u.write,...u.keychain?[M]:[],...Z({grant:h,extraWrite:o,cwd:t,home:c}),...D(h)]:[t,...D(h)]),denyRead:f([...a==="inverted"?["~",...c?[c]:[]]:[],...[...S,...j].filter(R=>!(u.keychain&&U.test(R))),...s])}})}const V=Object.freeze({noWrite:Object.freeze(["runtime/sandbox-runtime","runtime/ort","bin","node_modules","package.json","package-lock.json","git-hooks","policy.json","bridge-token","agent-threads.json","update-check.json","launch.vbs","workspace","worktrees","channels","desktop","cli","actions-runner","release.env"]),noRead:Object.freeze(["channels","desktop","cli","actions-runner","release.env","workspace","worktrees"])});function ue({stateDir:e,codeDirs:t=[],home:n="",mode:r="open",hosts:o=[],scratch:s=[],fence:l=V}={}){p(typeof e=="string"&&e.length>1,"gateway kit: stateDir required");const c=q({engine:"custom",cwd:e,mode:r,extraHosts:o,home:n,reads:"inverted",peers:!1}),a=u=>typeof u=="string"&&u.startsWith("/")&&u.replace(/\/+$/,"")!==""&&u!==n,i=t.filter(a),d=u=>`${e.replace(/\/+$/,"")}/${u}`;return $({...c,id:`gateway:${r}`,filesystem:{...c.filesystem,allowWrite:f([e,"/tmp","/private/tmp",...s.filter(a)]),denyWrite:f([...i,...l.noWrite.map(d)]),allowRead:f([...c.filesystem.allowRead,...i]),denyRead:f([...c.filesystem.denyRead,...l.noRead.map(d)])}})}function Q(e){if(!/[*?[{]/.test(e))return e;const t=e.split(/[\\/]/),n=t.findIndex(s=>/[*?[{]/.test(s)),r=e.includes("\\")&&!e.includes("/")?"\\":"/",o=t.slice(0,n).join(r);return o&&o!=="~"?o:""}function Z({grant:e=null,extraWrite:t=[],cwd:n="",home:r=""}={}){const o=i=>r&&(i==="~"||i.startsWith("~/")||i.startsWith("~\\"))?r+i.slice(1):i,s=i=>{let d=String(i||"").trim();for(;d.length>1&&(d.endsWith("/")||d.endsWith("\\"));)d=d.slice(0,-1);return d},l=(i,d)=>i===d||i.startsWith(`${d}/`)||i.startsWith(`${d}\\`),c=s(o(n)),a=[];for(const i of[...y(e).write,...Array.isArray(t)?t:[]]){const d=Q(s(o(s(i))));if(d&&!W(d,r)&&!(c&&l(d,c))&&!a.some(u=>l(d,u))){for(let u=a.length-1;u>=0;u--)l(a[u],d)&&a.splice(u,1);a.push(d)}}return a}function fe(e,{loopback:t=[],home:n="",platform:r="darwin"}={}){const o=$(e),s=c=>n&&c.startsWith("~")?n+c.slice(1):c,l=o.network.mode==="none"?[]:t.map(c=>`127.0.0.1:${c}`);return{network:{allowedDomains:f([...o.network.allowedDomains,...l]),deniedDomains:o.network.deniedDomains,allowLocalBinding:r==="darwin"&&l.length>0,...r==="darwin"&&o.network.unixSockets?.length?{allowUnixSockets:o.network.unixSockets}:{}},filesystem:{denyRead:o.filesystem.denyRead.map(s),allowRead:o.filesystem.allowRead.map(s),allowWrite:o.filesystem.allowWrite.map(s),denyWrite:o.filesystem.denyWrite.map(s)}}}function pe({layer:e="none",runtime:t=null,kitId:n=null,mode:r=null,reason:o=null,hosts:s=null}={}){return p(k.includes(e),`sandbox record: layer must be one of ${k.join(", ")}`),{layer:e,runtime:t,kit:n,mode:r,...e==="none"&&o?{reason:o}:{},...Array.isArray(s)&&s.length?{hosts:f(s)}:{}}}function J(e,{platform:t="darwin"}={}){const n=String(e);if(t==="win32"){p(!/[\r\n]/.test(n),"win32 command line: an argument holds a newline, which cmd.exe cannot carry");let r=n;if(n===""||/[ \t\v"]/.test(n)){r='"';for(let o=0;o<n.length;o++){let s=0;for(;o<n.length&&n[o]==="\\";)s++,o++;if(o===n.length){r+="\\".repeat(s*2);break}n[o]==='"'?r+="\\".repeat(s*2+1)+'"':r+="\\".repeat(s)+n[o]}r+='"'}return r.replace(/[()%!^"<>&|]/g,"^$&")}return n===""?"''":/^[A-Za-z0-9_\-./=:@%+,]+$/.test(n)?n:`'${n.replace(/'/g,"'\\''")}'`}const ee=Object.freeze(["SSL_CERT_FILE","REQUESTS_CA_BUNDLE","CURL_CA_BUNDLE","GIT_SSL_CAINFO"]);function ge(e={},{platform:t="darwin",caBundle:n=""}={}){const r={NODE_USE_ENV_PROXY:"1"};if(n)for(const o of ee)e[o]||(r[o]=n);if(t==="linux")for(const o of["no_proxy","NO_PROXY"])typeof e[o]=="string"&&(r[o]=e[o].split(",").map(s=>s.trim()).filter(s=>s&&!/^(localhost|127\.0\.0\.1|::1|\[::1\])$/i.test(s)).join(","));return r}function me(e,t=[],{platform:n="darwin"}={}){return[e,...t].map(r=>J(r,{platform:n})).join(" ")}const we=/[\\/]+/,w=e=>String(e||"").replace(/\//g,"\\").replace(/\\+$/,"").toLowerCase(),te=e=>/^[a-z]:$/i.test(w(e))||w(e)==="";function b(e,t){const n=w(e),r=w(t);return!!r&&n!==r&&n.startsWith(r+"\\")}function ne(e){const t=String(e||"").replace(/\//g,"\\").split("\\");if(t.length<2)return"";const n=t.findIndex(r=>r.toLowerCase()==="node_modules");return(n>0?t.slice(0,n):t.slice(0,-1)).join("\\")}function he({kit:e,files:t=[],home:n}){p(typeof n=="string"&&n.length>0,"windows grants: home required");const r=a=>(a.startsWith("~")?n+a.slice(1):a).replace(/\//g,"\\"),o=[];for(const a of e?.filesystem?.allowWrite||[]){const i=r(a);!/^[a-z]:\\/i.test(i)&&!i.startsWith("\\\\")||(p(!te(i)&&w(i)!==w(n),`windows grants: a write on ${i} would hand the sandbox account the whole profile`),b(i,n)&&o.push(i))}const s=(a,i)=>i.some(d=>w(a)===w(d)||b(a,d)),l=[];for(const a of t){const i=ne(a);i&&b(i,n)&&l.push(i)}for(const a of e?.filesystem?.allowRead||[]){if(/[*?[]/.test(a))continue;const i=r(a);b(i,n)&&l.push(i)}const c=f(l).filter(a=>!s(a,o));return{read:c.filter(a=>!c.some(i=>i!==a&&b(a,i))),write:f(o)}}const oe=Object.freeze(["ANTHROPIC_API_KEY","ANTHROPIC_AUTH_TOKEN","ANTHROPIC_BASE_URL","CLAUDE_CONFIG_DIR","OPENAI_API_KEY","OPENAI_BASE_URL","OPENAI_API_BASE","CODEX_HOME","GEMINI_API_KEY","GOOGLE_API_KEY","DEEPSEEK_API_KEY","DSH_HOME","OPENROUTER_API_KEY","GROQ_API_KEY","XAI_API_KEY","MISTRAL_API_KEY"]);function ye({engine:e="custom",home:t,env:n={},base:r={},carry:o=oe}={}){p(typeof t=="string"&&t.length>0,"windows env: home required");const s=t.replace(/\//g,"\\"),l=/^[a-z]:/i.test(s)?s.slice(0,2):"",c={USERPROFILE:s,...l?{HOMEDRIVE:l,HOMEPATH:s.slice(2)}:{}};for(const[a,i]of Object.entries(n))typeof i=="string"&&(r[a]!==i||o.includes(a))&&(c[a]=i);return e==="codex"&&!c.CODEX_HOME&&(c.CODEX_HOME=s+"\\.codex"),c}function Ee(e,t){p(Array.isArray(e),"srt-win argv: must be an array");const n=e.indexOf("exec"),r=n>=0?e.indexOf("--",n+1):-1;p(n>=0&&r>n,"srt-win argv: expected `exec … -- <shell> <command>`");const o=new Set;for(let l=n+1;l<r;l++)e[l]==="--env"&&typeof e[l+1]=="string"&&o.add(e[l+1].split("=")[0].toUpperCase());const s=[];for(const[l,c]of Object.entries(t||{}))p(/^[A-Za-z_][A-Za-z0-9_]*$/.test(l),`srt-win env: bad key ${l}`),p(!/[\r\n]/.test(c),`srt-win env: ${l} holds a newline`),o.has(l.toUpperCase())||s.push("--env",`${l}=${c}`);return[...e.slice(0,r),...s,...e.slice(r)]}export{C as CA_BUNDLE_RELPATH,ce as CA_BUNDLE_RELPATH_LEGACY,z as COMMON_WRITE,F as DEVELOPER_HOSTS,v as ENGINE_KITS,V as GATEWAY_FENCE,N as NETWORK_MODES,k as SANDBOX_LAYERS,ke as SANDBOX_READ_MODES,Ne as SANDBOX_RUNTIMES,S as SECRET_PATHS,j as SECRET_READ_GLOBS,Ie as SIGN_IN_LABELS,Re as SIGN_IN_NAMES,I as SIGN_IN_STORES,B as TOOLCHAIN_READ,oe as WINDOWS_CARRY_KEYS,le as caBundlePath,je as expandWrapper,ue as gatewayKit,W as isSecretWritePath,q as kitFor,Se as mergeGrants,Oe as mergePersonGrants,xe as normalizeGrant,Ae as parseHostList,We as parseWrapperTemplate,ae as readDenialFor,ge as sandboxEnvOverlay,pe as sandboxRecord,me as shellJoin,J as shellQuote,de as signInDenial,Y as signInHosts,D as signInRead,fe as toSrtSettings,b as underWindowsHome,$ as validateKit,he as windowsGrantsFor,ne as windowsInstallRoot,ye as windowsSandboxEnv,Ee as withSrtWinEnv,Z as writableRoots};