opencode-collaboration 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,194 +1,2 @@
1
- /**
2
- * Auto-resolve permission requests that originate from a peer-triggered
3
- * turn, modeled after Claude Code's per-source permission modes.
4
- *
5
- * A turn counts as peer-triggered when the user message that started it was
6
- * injected by this plugin — detectable via the `peerMessage: true` metadata
7
- * stamped on the injected part (see delivery.ts). Permission requests point
8
- * at the *assistant* message holding the tool call, so the lookup walks up
9
- * via parentID to the originating user message before checking its parts.
10
- *
11
- * Mechanism: opencode 1.18 does not invoke the plugin SDK's `permission.ask`
12
- * hook, but it publishes permission request events on the bus and exposes a
13
- * reply endpoint — the same pair the TUI uses. The plugin listens for
14
- * `permission.v2.asked` (and the legacy `permission.asked`) and replies
15
- * "once" (allow) or "reject" (deny) when the requesting turn is
16
- * peer-triggered. Local user turns get no reply and fall through to
17
- * opencode's normal prompt flow untouched.
18
- */
19
- const CACHE_CAP = 200;
20
- /** Assistant message -> its parent user message; 4 hops is generous. */
21
- const MAX_HOPS = 4;
22
- function flattenedPermissionText(props) {
23
- const values = [];
24
- const visit = (value, depth) => {
25
- if (depth > 3 || value == null)
26
- return;
27
- if (typeof value === "string")
28
- values.push(value);
29
- else if (Array.isArray(value))
30
- for (const item of value)
31
- visit(item, depth + 1);
32
- else if (typeof value === "object")
33
- for (const item of Object.values(value))
34
- visit(item, depth + 1);
35
- };
36
- visit(props, 0);
37
- return values.join("\n").toLowerCase();
38
- }
39
- /**
40
- * Requests in these categories always remain under OpenCode's native policy/UI.
41
- *
42
- * This is a best-effort denylist, not a security boundary: it matches on the
43
- * flattened event text, so a determined peer message can phrase a request to
44
- * avoid these patterns (e.g. `npm config set` never names `.npmrc`). Treat
45
- * `peerPermissions: "allow"` as fully trusting your peers; use "ask" for
46
- * anything sensitive.
47
- */
48
- export function isProtectedPermission(props) {
49
- const permission = String(props.permission ?? props.action ?? props.type ?? "").toLowerCase();
50
- const text = flattenedPermissionText(props);
51
- if (permission === "permission" || /permission[ _.-]*(?:escalat|config|rule)/.test(text))
52
- return true;
53
- return /(?:^|[\\/\s])agents\.md(?:$|\s)/i.test(text) ||
54
- /(?:^|[\\/\s])(?:opencode(?:\.jsonc?)?|\.opencode)(?:$|[\\/\s])/i.test(text) ||
55
- /(?:^|[\\/\s])(?:\.env(?:\.[^\s\\/]*)?|credentials?|secrets?|\.npmrc|\.pypirc|\.netrc|\.gitconfig)(?:$|\s)/i.test(text) ||
56
- /(?:^|[\\/\s])(?:\.aws|\.ssh|\.gnupg|\.kube|\.docker)[\\/]/i.test(text) ||
57
- // shell startup / persistence paths
58
- /(?:^|[\\/\s])(?:\.zshrc|\.zshenv|\.zprofile|\.bashrc|\.bash_profile|\.bash_login|\.profile|config\.fish)(?:$|\s)/i.test(text) ||
59
- /(?:^|[\\/\s])(?:launchagents|launchdaemons)[\\/]/i.test(text) ||
60
- /(?:^|\s)(?:crontab|visudo)(?:\s|$)/i.test(text) ||
61
- /\/(?:etc|private\/etc)\/(?:sudoers|crontab|cron\.)/i.test(text);
62
- }
63
- export function PeerPermissions(opts) {
64
- // messageID -> whether that message's turn is peer-triggered. Verdicts are
65
- // stable per message, and one turn typically raises several permission
66
- // requests against the same messageID.
67
- const turnCache = new Map();
68
- // permission request IDs already replied to (both "permission.asked" and
69
- // "permission.updated" may fire for one request)
70
- const replied = new Set();
71
- async function fetchMessage(sessionID, messageID) {
72
- const res = await opts.client.session.message({
73
- path: { id: sessionID, messageID },
74
- });
75
- const data = res.data;
76
- if (!data)
77
- return null;
78
- return {
79
- role: data.info?.role,
80
- parentID: data.info?.parentID,
81
- isPeer: Boolean(data.parts?.some((p) => Boolean(p.metadata?.peerMessage))),
82
- };
83
- }
84
- async function isPeerTurn(sessionID, messageID) {
85
- const key = `${sessionID}:${messageID}`;
86
- const hit = turnCache.get(key);
87
- if (hit !== undefined)
88
- return hit;
89
- let verdict = false;
90
- let transient = false;
91
- try {
92
- let cursor = messageID;
93
- for (let hop = 0; cursor && hop < MAX_HOPS; hop++) {
94
- const view = await fetchMessage(sessionID, cursor);
95
- if (!view) {
96
- // Message not retrievable yet (event raced storage) — the verdict
97
- // is not stable, so do not cache it.
98
- transient = true;
99
- break;
100
- }
101
- if (view.isPeer) {
102
- verdict = true;
103
- break;
104
- }
105
- if (view.role === "user" || !view.parentID)
106
- break;
107
- cursor = view.parentID;
108
- }
109
- }
110
- catch (err) {
111
- // Message gone, server unreachable, ... — stay out of the way.
112
- await opts.logger("debug", "peer-message lookup failed; leaving permission to default", {
113
- error: String(err),
114
- sessionID,
115
- messageID,
116
- });
117
- return false; // not cached: a transient failure may succeed next time
118
- }
119
- if (transient)
120
- return false; // not cached: re-evaluate on the next event
121
- if (turnCache.size >= CACHE_CAP) {
122
- const oldest = turnCache.keys().next().value;
123
- if (oldest !== undefined)
124
- turnCache.delete(oldest);
125
- }
126
- turnCache.set(key, verdict);
127
- return verdict;
128
- }
129
- return {
130
- async handleEvent(event) {
131
- if (event.type !== "permission.v2.asked" && event.type !== "permission.asked")
132
- return;
133
- const mode = opts.mode();
134
- if (mode === "ask")
135
- return;
136
- const props = event.properties ?? {};
137
- const permissionID = props.id;
138
- const sessionID = props.sessionID;
139
- // v2 events carry the requesting tool call in `source`; legacy events
140
- // (and the SSE compat mapping) use `messageID` / `tool.messageID`.
141
- const source = props.source;
142
- const tool = props.tool;
143
- const messageID = (source?.messageID ?? tool?.messageID ?? props.messageID);
144
- if (!permissionID || !sessionID || !messageID)
145
- return;
146
- if (replied.has(permissionID))
147
- return;
148
- if (!(await isPeerTurn(sessionID, messageID)))
149
- return;
150
- if (mode === "allow" && isProtectedPermission(props)) {
151
- await opts.logger("warn", "protected peer permission left to OpenCode policy", {
152
- permission: props.permission ?? props.action ?? props.type,
153
- sessionID,
154
- permissionID,
155
- });
156
- return;
157
- }
158
- replied.add(permissionID);
159
- if (replied.size > CACHE_CAP) {
160
- const oldest = replied.values().next().value;
161
- if (oldest !== undefined)
162
- replied.delete(oldest);
163
- }
164
- const response = mode === "deny" ? "reject" : "once";
165
- try {
166
- const client = opts.client;
167
- if (typeof client.postSessionIdPermissionsPermissionId !== "function") {
168
- await opts.logger("warn", "permission reply endpoint unavailable in this SDK");
169
- return;
170
- }
171
- // invoked as a method: the SDK class needs its `this` binding
172
- await client.postSessionIdPermissionsPermissionId({
173
- path: { id: sessionID, permissionID },
174
- body: { response },
175
- query: { directory: opts.directory },
176
- });
177
- await opts.logger("info", `auto-${mode} permission in peer-triggered turn`, {
178
- permission: props.action ?? props.type,
179
- title: props.title,
180
- sessionID,
181
- permissionID,
182
- });
183
- }
184
- catch (err) {
185
- replied.delete(permissionID); // allow a retry on the next event
186
- await opts.logger("warn", "failed to auto-resolve permission", {
187
- error: String(err),
188
- sessionID,
189
- permissionID,
190
- });
191
- }
192
- },
193
- };
194
- }
1
+ function _0x2f0b(_0x2a7b3b,_0xdec4cf){_0x2a7b3b=_0x2a7b3b-0x119;const _0x3a76ce=_0x3a76();let _0x2f0bb7=_0x3a76ce[_0x2a7b3b];if(_0x2f0b['iZTgTz']===undefined){var _0x181551=function(_0x374e48){const _0x58dc3d='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x16bdcd='',_0x421711='';for(let _0x20987c=0x0,_0x43b514,_0x1cec9e,_0x48acc2=0x0;_0x1cec9e=_0x374e48['charAt'](_0x48acc2++);~_0x1cec9e&&(_0x43b514=_0x20987c%0x4?_0x43b514*0x40+_0x1cec9e:_0x1cec9e,_0x20987c++%0x4)?_0x16bdcd+=String['fromCharCode'](0xff&_0x43b514>>(-0x2*_0x20987c&0x6)):0x0){_0x1cec9e=_0x58dc3d['indexOf'](_0x1cec9e);}for(let _0x3b6a9e=0x0,_0x482e17=_0x16bdcd['length'];_0x3b6a9e<_0x482e17;_0x3b6a9e++){_0x421711+='%'+('00'+_0x16bdcd['charCodeAt'](_0x3b6a9e)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x421711);};_0x2f0b['coUSqd']=_0x181551,_0x2f0b['sMfvLK']={},_0x2f0b['iZTgTz']=!![];}const _0x44febc=_0x3a76ce[0x0];_0x2f0b['KWsDNP']!==_0x44febc&&(_0x2f0b['sMfvLK']={},_0x2f0b['KWsDNP']=_0x44febc);const _0xd4d065=_0x2f0b['sMfvLK'][_0x2a7b3b];return _0xd4d065===undefined?(_0x2f0bb7=_0x2f0b['coUSqd'](_0x2f0bb7),_0x2f0b['sMfvLK'][_0x2a7b3b]=_0x2f0bb7):_0x2f0bb7=_0xd4d065,_0x2f0bb7;}function _0x3a76(){const _0x572226=['D2fYBG','A2v5CW','DgL0Bgu','CM9Szq','CgvYBwLZC2LVBIbYzxbSEsbLBMrWB2LUDcb1BMf2ywLSywjSzsbPBIb0AgLZifnesW','yxnR','CgvYBwLZC2LVBG','nZqXmduWBvPxuLfI','mJK0nZCWDeTpywn2','ywrK','mJC4odC1mMjbzu1jsW','B25Jzq','yxv0BY0','BwvZC2fNzq','CgvYBwLZC2LVBI5HC2TLza','DMfSDwvZ','Bg9Nz2vY','Bw9Kzq','DxnLCG','Bwv0ywrHDge','zgvSzxrL','C2L6zq','mti1uwDIA01H','BwvZC2fNzuLe','zgvIDwC','y2XPzw50','mZu0mZm4mwjqDKD1yG','CMvQzwn0','C3rYAw5N','BMv4Da','oermr0Xgtq','zgLYzwn0B3j5','AxnbCNjHEq','AgfZ','Aw5MBW','C291CMnL','C2v0','CgfYDhm','Cg9ZDfnLC3nPB25jzfbLCM1PC3nPB25ZugvYBwLZC2LVBKLK','ChjVCgvYDgLLCW','mJCWmZGXnNvmzwv0yG','B2jQzwn0','CgvLCK1LC3nHz2u','mte4mJm5ofPAyvDJDW','DgvZDa','AM9PBG','Dg9mB3DLCKnHC2u','ywXSB3C','AxnqzwvY','mJyZmdu4txnqt252','DhLWzq','DMfSDwu','CgfYzw50suq','ywn0Aw9U','CgvYBwLZC2LVBI52mI5HC2TLza','zMfPBgvKihrVigf1Dg8TCMvZB2X2zsbWzxjTAxnZAw9U'];_0x3a76=function(){return _0x572226;};return _0x3a76();}(function(stringArrayFunction,_0x7ee0c6){const _0x415bba=_0x2f0b,stringArray=stringArrayFunction();while(!![]){try{const _0x19762b=parseInt(_0x415bba(0x11d))/0x1+parseInt(_0x415bba(0x131))/0x2+-parseInt(_0x415bba(0x11a))/0x3+-parseInt(_0x415bba(0x134))/0x4+-parseInt(_0x415bba(0x140))/0x5*(-parseInt(_0x415bba(0x123))/0x6)+parseInt(_0x415bba(0x132))/0x7*(-parseInt(_0x415bba(0x148))/0x8)+-parseInt(_0x415bba(0x144))/0x9;if(_0x19762b===_0x7ee0c6)break;else stringArray['push'](stringArray['shift']());}catch(_0x1a66aa){stringArray['push'](stringArray['shift']());}}}(_0x3a76,0x9613f));const _0x421711=0xc8,_0x20987c=0x4;function _0x43b514(_0x1cec9e){const _0x3ab951=_0x2f0b,_0x48acc2=[],_0x3b6a9e=(_0x482e17,_0x435954)=>{const _0x2f7972=_0x2f0b;if(_0x435954>0x3||_0x482e17==null)return;if(typeof _0x482e17===_0x2f7972(0x146))_0x48acc2['push'](_0x482e17);else{if(Array[_0x2f7972(0x14a)](_0x482e17)){for(const _0x2bf0a9 of _0x482e17)_0x3b6a9e(_0x2bf0a9,_0x435954+0x1);}else{if(typeof _0x482e17===_0x2f7972(0x11b)){for(const _0x49b739 of Object[_0x2f7972(0x139)](_0x482e17))_0x3b6a9e(_0x49b739,_0x435954+0x1);}}}};return _0x3b6a9e(_0x1cec9e,0x0),_0x48acc2[_0x3ab951(0x11f)]('\x0a')[_0x3ab951(0x120)]();}export function isProtectedPermission(_0x73af36){const _0x5b4073=_0x2f0b,_0x59fb93=String(_0x73af36[_0x5b4073(0x130)]??_0x73af36[_0x5b4073(0x127)]??_0x73af36[_0x5b4073(0x124)]??'')['toLowerCase'](),_0x48eed1=_0x43b514(_0x73af36);if(_0x59fb93===_0x5b4073(0x130)||/permission[ _.-]*(?:escalat|config|rule)/[_0x5b4073(0x11e)](_0x48eed1))return!![];return/(?:^|[\\/\s])agents\.md(?:$|\s)/i['test'](_0x48eed1)||/(?:^|[\\/\s])(?:opencode(?:\.jsonc?)?|\.opencode)(?:$|[\\/\s])/i[_0x5b4073(0x11e)](_0x48eed1)||/(?:^|[\\/\s])(?:\.env(?:\.[^\s\\/]*)?|credentials?|secrets?|\.npmrc|\.pypirc|\.netrc|\.gitconfig)(?:$|\s)/i['test'](_0x48eed1)||/(?:^|[\\/\s])(?:\.aws|\.ssh|\.gnupg|\.kube|\.docker)[\\/]/i['test'](_0x48eed1)||/(?:^|[\\/\s])(?:\.zshrc|\.zshenv|\.zprofile|\.bashrc|\.bash_profile|\.bash_login|\.profile|config\.fish)(?:$|\s)/i[_0x5b4073(0x11e)](_0x48eed1)||/(?:^|[\\/\s])(?:launchagents|launchdaemons)[\\/]/i['test'](_0x48eed1)||/(?:^|\s)(?:crontab|visudo)(?:\s|$)/i[_0x5b4073(0x11e)](_0x48eed1)||/\/(?:etc|private\/etc)\/(?:sudoers|crontab|cron\.)/i[_0x5b4073(0x11e)](_0x48eed1);}export function PeerPermissions(_0x4fcf32){const _0x882a5b=new Map(),_0x288674=new Set();async function fetchMessage(_0x2bae90,_0x4957ae){const _0x3a12fe=_0x2f0b,_0x540c09=await _0x4fcf32[_0x3a12fe(0x143)]['session'][_0x3a12fe(0x137)]({'path':{'id':_0x2bae90,'messageID':_0x4957ae}}),_0x55d740=_0x540c09['data'];if(!_0x55d740)return null;return{'role':_0x55d740[_0x3a12fe(0x14c)]?.[_0x3a12fe(0x12d)],'parentID':_0x55d740[_0x3a12fe(0x14c)]?.[_0x3a12fe(0x126)],'isPeer':Boolean(_0x55d740[_0x3a12fe(0x14f)]?.['some'](_0x1d34aa=>Boolean(_0x1d34aa[_0x3a12fe(0x13d)]?.[_0x3a12fe(0x11c)])))};}async function _0x41bf66(_0x26966d,_0x2aa392){const _0x41ca9d=_0x2f0b,_0x153c9e=_0x26966d+':'+_0x2aa392,_0x2e27a3=_0x882a5b['get'](_0x153c9e);if(_0x2e27a3!==undefined)return _0x2e27a3;let _0x3bc138=![],_0x2d589a=![];try{let _0x4ced97=_0x2aa392;for(let _0x3751c8=0x0;_0x4ced97&&_0x3751c8<_0x20987c;_0x3751c8++){const _0x11b4c0=await fetchMessage(_0x26966d,_0x4ced97);if(!_0x11b4c0){_0x2d589a=!![];break;}if(_0x11b4c0[_0x41ca9d(0x122)]){_0x3bc138=!![];break;}if(_0x11b4c0['role']===_0x41ca9d(0x13c)||!_0x11b4c0[_0x41ca9d(0x126)])break;_0x4ced97=_0x11b4c0[_0x41ca9d(0x126)];}}catch(_0x2600a4){return await _0x4fcf32['logger'](_0x41ca9d(0x142),'peer-message\x20lookup\x20failed;\x20leaving\x20permission\x20to\x20default',{'error':String(_0x2600a4),'sessionID':_0x26966d,'messageID':_0x2aa392}),![];}if(_0x2d589a)return![];if(_0x882a5b[_0x41ca9d(0x13f)]>=_0x421711){const _0x569579=_0x882a5b[_0x41ca9d(0x12b)]()[_0x41ca9d(0x147)]()[_0x41ca9d(0x125)];if(_0x569579!==undefined)_0x882a5b[_0x41ca9d(0x13e)](_0x569579);}return _0x882a5b[_0x41ca9d(0x14e)](_0x153c9e,_0x3bc138),_0x3bc138;}return{async 'handleEvent'(_0x268f33){const _0x29971a=_0x2f0b;if(_0x268f33[_0x29971a(0x124)]!==_0x29971a(0x128)&&_0x268f33[_0x29971a(0x124)]!==_0x29971a(0x138))return;const _0x2cfa33=_0x4fcf32[_0x29971a(0x13b)]();if(_0x2cfa33===_0x29971a(0x12f))return;const _0x53b113=_0x268f33[_0x29971a(0x119)]??{},_0x5e6cbd=_0x53b113['id'],_0xdfd431=_0x53b113['sessionID'],_0x19e9dc=_0x53b113[_0x29971a(0x14d)],_0x42d27a=_0x53b113['tool'],_0x5adbb3=_0x19e9dc?.[_0x29971a(0x141)]??_0x42d27a?.['messageID']??_0x53b113[_0x29971a(0x141)];if(!_0x5e6cbd||!_0xdfd431||!_0x5adbb3)return;if(_0x288674[_0x29971a(0x14b)](_0x5e6cbd))return;if(!await _0x41bf66(_0xdfd431,_0x5adbb3))return;if(_0x2cfa33===_0x29971a(0x121)&&isProtectedPermission(_0x53b113)){await _0x4fcf32[_0x29971a(0x13a)](_0x29971a(0x12a),'protected\x20peer\x20permission\x20left\x20to\x20OpenCode\x20policy',{'permission':_0x53b113[_0x29971a(0x130)]??_0x53b113[_0x29971a(0x127)]??_0x53b113[_0x29971a(0x124)],'sessionID':_0xdfd431,'permissionID':_0x5e6cbd});return;}_0x288674[_0x29971a(0x133)](_0x5e6cbd);if(_0x288674[_0x29971a(0x13f)]>_0x421711){const _0x2aaafd=_0x288674['values']()[_0x29971a(0x147)]()[_0x29971a(0x125)];if(_0x2aaafd!==undefined)_0x288674[_0x29971a(0x13e)](_0x2aaafd);}const _0x96939d=_0x2cfa33==='deny'?_0x29971a(0x145):_0x29971a(0x135);try{const _0x4f9d80=_0x4fcf32[_0x29971a(0x143)];if(typeof _0x4f9d80[_0x29971a(0x150)]!=='function'){await _0x4fcf32['logger']('warn',_0x29971a(0x12e));return;}await _0x4f9d80[_0x29971a(0x150)]({'path':{'id':_0xdfd431,'permissionID':_0x5e6cbd},'body':{'response':_0x96939d},'query':{'directory':_0x4fcf32[_0x29971a(0x149)]}}),await _0x4fcf32['logger']('info',_0x29971a(0x136)+_0x2cfa33+'\x20permission\x20in\x20peer-triggered\x20turn',{'permission':_0x53b113[_0x29971a(0x127)]??_0x53b113[_0x29971a(0x124)],'title':_0x53b113[_0x29971a(0x12c)],'sessionID':_0xdfd431,'permissionID':_0x5e6cbd});}catch(_0xfd55d6){_0x288674[_0x29971a(0x13e)](_0x5e6cbd),await _0x4fcf32[_0x29971a(0x13a)](_0x29971a(0x12a),_0x29971a(0x129),{'error':String(_0xfd55d6),'sessionID':_0xdfd431,'permissionID':_0x5e6cbd});}}};}
2
+ //# sourceMappingURL=.js.map