Dashboard › opencode › Distillation
5fcf975a-5bdc-47f1-87d9-564bfe870624["lore_tm_v1_QeYz3o3o7CvhJmqBJo3XKvl6TpQW5f9-D2yaNAlnh84","lore_tm_v1_BA0SZyE1bkzgfrDicGFqIXinacew171iZpMu7JGp7TU","lore_tm_v1_T5H4BaPzPfap38clOeigfKG5Z57SWWKjpQ8mNn9F040"]
/home/byk/Code/opencode-v2-pilot/packages/core/src/permission.ts is 332 lines and exports Core permission schemas/types ID, Source, Request, Reply, AssertInput, ReplyInput, AskResult, Rule, Ruleset, Event, and PermissionEffect./home/byk/Code/opencode-v2-pilot/packages/core/src/permission.ts:42-47 defines AssertInput from optional id, shared RequestFields (sessionID, action, resources, save, metadata, source), and optional agent: Agent.ID./home/byk/Code/opencode-v2-pilot/packages/core/src/permission.ts:49-60 defines ReplyInput with requestID, reply, and optional message; AskResult contains id and effect.DeclinedError, CorrectedError { feedback }, BlockedError { rules, permission, resources, reason? }, and NotFoundError { requestID }; BlockedError.message is reason ?? \Permission denied: ${this.permission}`, while exported ErrorisBlockedError | CorrectedError`./home/byk/Code/opencode-v2-pilot/packages/core/src/permission.ts:87-101 defines evaluate(action, resource, ...rulesets): it flattens rulesets, chooses the last wildcard-matching rule with findLast, and defaults to { action, resource: "*", effect: "ask" }; merge(...rulesets) simply flattens them.Interface exposes ask(input), assert(input), reply(input), get(id), forSession(sessionID), and list(); ask and assert can fail with SessionErrors.NotFoundError, assert additionally exposes Core permission Error, and reply can fail with permission NotFoundError.Bus.Service, Location.Service, Agent.Service, SessionStore.Service, PermissionSaved.Service, and PluginHooks.Service; Permission.node declares the corresponding node dependencies.Map<ID, Pending>, where each Pending holds request, optional agent, and Deferred<void, DeclinedError | CorrectedError>; the layer finalizer fails all pending deferreds with DeclinedError and then clears the map.missingAgentPermissions is [{ action: "*", resource: "*", effect: "deny" }]; configured(sessionID, agentID?) loads the session, resolves agentID ?? session.agent, returns agent.permissions, and falls back to missingAgentPermissions.savedRules() loads saved permissions for location.project.id and converts each to { action, resource, effect: "allow" }.evaluateInput(input) first evaluates configured agent rules and immediately returns deny if any requested resource is denied; otherwise it appends saved allow rules, computes per-resource effects, chooses ask if any resource asks and allow otherwise, then triggers plugin hook hooks.trigger("permission", "evaluate", { sessionID, agent, action, resources, metadata, source, effect }) and accepts the hook-mutated effect and message.request(input, message?) creates a permission request using input.id ?? ID.create() and preserves sessionID, action, resources, save, metadata, source, and the evaluation message.create(request, agent?) runs uninterruptibly, defects on duplicate IDs with Duplicate pending permission ID: ${request.id}, inserts a deferred-backed pending entry, publishes Permission.Event.Asked, and removes the map entry if publication fails.Permission.ask evaluates input, constructs a request, creates a pending request only when the effect is "ask", and returns { id, effect }; it does not throw BlockedError for a "deny" result.Permission.assert evaluates input and uses Effect.uninterruptibleMask: "deny" fails with BlockedError containing action-relevant rules, action as permission, resources, and hook message as reason; "allow" returns; "ask" creates a pending request and waits interruptibly on its deferred.Permission.assert, Permission.DeclinedError is deliberately tunneled into a defect with Effect.die(error) so blanket leaf mapError handling cannot turn a user decline into model-facing tool output; CorrectedError remains typed so a leaf can convert feedback into ToolFailure; pending entries are deleted in Effect.ensuring.Permission.reply publishes Permission.Event.Replied; a "reject" reply fails the selected deferred with CorrectedError({ feedback: input.message }) when a message exists or DeclinedError otherwise, then rejects all other pending requests for the same session with DeclinedError.Permission.reply with "always" and nonempty request.save persists an allow through saved.add({ projectID: location.project.id, action, resources: save }); after resolving the selected request, it reevaluates all pending requests and automatically publishes "always" and resolves those newly allowed, ignoring Session.NotFoundError.list() returns all, get(id) returns one or undefined, and forSession(sessionID) filters by session./home/byk/.local/share/opencode-v2-pilot/config/opencode/plugins/pty.ts is an 852-line Promise plugin with ID "local-pty"; createPtyPlugin(options) accepts optional openProcess and spawnTimeoutMs, and the fileβs default export is createPtyPlugin().MAX_SESSIONS=16, MAX_SESSIONS_PER_OWNER=5, DEFAULT_READ_LIMIT=500, MAX_READ_LIMIT=2_000, MAX_READ_OFFSET=10_000, MAX_LINE_LENGTH=2_000, MAX_PATTERN_LENGTH=500, MAX_WRITE_SIZE=65_536, MAX_TIMEOUT_SECONDS=3_600, CLOSE_TIMEOUT_MS=27_000, NOTIFICATION_TIMEOUT_MS=5_000, IO_TIMEOUT_MS=NOTIFICATION_TIMEOUT_MS, MAX_COMMAND_LENGTH=4_096, MAX_ARGUMENTS=128, MAX_ARGUMENT_LENGTH=16_384, MAX_ARGUMENT_BYTES=65_536, MAX_PATH_LENGTH=4_096, MAX_TITLE_LENGTH=500, MAX_DESCRIPTION_LENGTH=2_000, MAX_REGEX_WORKERS=4, MAX_BUFFER_SIZE=1_000_000, TERMINATE_TIMEOUT_MS=2_000, and SPAWN_TIMEOUT_MS=5_000.SCRIPT_PATH="/usr/bin/script" and SHELL_PATH="/bin/sh" with fixed environment HOME=/home/byk, LANG=C.UTF-8, LOGNAME=byk, PATH=/usr/bin:/bin, SHELL=/bin/sh, TERM=xterm-256color, and USER=byk.SpawnInput requires command, args, and a nonempty description; optionally accepts workdir, title, notifyOnExit, and integer timeoutSeconds from 1 through MAX_TIMEOUT_SECONDS. Command, argument, path, title, and description schemas enforce the corresponding length limits.WriteInput takes id and data; data supports \n, \r, \t, \xNN, \uNNNN, and \\, and is limited to MAX_WRITE_SIZE after UTF-8 escape decoding.ReadInput supports optional offset (0βMAX_READ_OFFSET), limit (1βMAX_READ_LIMIT, default 500), extended-regex pattern, and ignoreCase; KillInput has id and optional cleanup."running" | "killing" | "killed" | "exited"; each session retains ID, title, description, command/args, workdir, status, notification and timeout state, exit data, PID, creation timestamp, owning parentSessionID, process, timer/closure/termination state, notification flag, and a bounded output buffer.sessions, deleted parent session IDs, global and per-owner spawn reservations, pending spawns, owner-specific spawns, pending notifications, closing state, and active regex workers.ctx.session.synthetic with delivery: "steer", description PTY exited: ..., and JSON containing type: "pty.exit", session ID, truncated description, exit code/signal, timeout data, output line count, and truncated last nonblank line; notifications time out after NOTIFICATION_TIMEOUT_MS and are suppressed if already sent, disabled, closing, or the parent session was deleted.pty_spawn, 2. pty_write, 3. pty_read, 4. pty_list, 5. pty_kill.pty_spawn uses { codemode: false, permission: "shell" }; it validates command input, rejects plugin closure/deleted sessions, enforces 5 sessions per owner and 16 per Location including spawn reservations, and may evict an ownerβs first inactive ("exited" or "killed") session when at the owner limit.pty_spawn calls authorize(...) before process launch, creates IDs as pty_${crypto.randomUUID().replaceAll("-", "").slice(0, 8)}, uses options.spawnTimeoutMs ?? SPAWN_TIMEOUT_MS, closes directory handles after launch, and closes late-arriving processes if startup times out.notifyOnExit to false, stores the authorized real directory, appends output while retaining only the last MAX_BUFFER_SIZE characters, records exit code/signal, and marks natural or failed closure as "exited".timeoutSeconds timer sets timedOut=true and invokes stop(session); spawn reservations and pending/owner spawn tracking are released in operation.finally().pty_write uses { codemode: false, permission: "shell" }; it requires same-session ownership and "running" state, decodes escapes, validates well-formed Unicode and post-decoding UTF-8 byte size, calls assertShell, rechecks ownership/state after approval, then writes with IO_TIMEOUT_MS.pty_read uses { codemode: false }; it returns paged lines or regex matches with line numbers and text truncated to MAX_LINE_LENGTH, plus status, pattern, offset, limit, total lines, hasMore, and nextOffset; regex reads are limited to MAX_REGEX_WORKERS.pty_list uses { codemode: false } and lists only PTYs whose parentSessionID matches the current session.pty_kill uses { codemode: false, permission: "shell" }; it obtains shell permission and rechecks ownership afterward, optionally removes the session and clears output with cleanup=true, otherwise stops running/killing sessions but retains their records.session.deleted; it marks the parent session deleted, waits up to CLOSE_TIMEOUT_MS for that ownerβs pending spawns, removes all owned PTYs, and logs cleanup failures. Plugin unload sets closing=true, aborts event subscription, and waits up to CLOSE_TIMEOUT_MS for pending spawns, session removals, and pending notifications.openPtyProcess() launches /usr/bin/script detached with arguments ["-q", "-e", "-f", "-c", ... , "/dev/null"], uses the authorized directory via cwd: /proc/self/fd/${directory.fd}, and receives the actual command PID through file descriptor 3 while retaining the wrapper PID.openPtyProcess() uses StringDecoder("utf8") for stdout/stderr, buffers pre-listener output up to MAX_BUFFER_SIZE, validates the reported PID as a positive decimal line no longer than 32 characters, and kills the process group if startup fails or times out.SIGTERM to both command and wrapper process groups, waits TERMINATE_TIMEOUT_MS, escalates both to SIGKILL, then verifies process closure and process-group disappearance; exit codes 143 and 137 are normalized to SIGTERM and SIGKILL.terminate(session) is idempotent via session.terminate, changes status to "killing", gives session.process.close() and session.closed each up to CLOSE_TIMEOUT_MS, then marks the session "killed".authorize() resolves the requested workdir and project directory, opens the directory with O_RDONLY | O_DIRECTORY | O_NOFOLLOW, verifies it is still the same directory through /proc/self/fd, asks external_directory permission when outside the project, then asks shell permission; both assertions include sessionID, agent, empty save, and tool source { type: "tool", messageID, id }.authorize() guards against workdir TOCTOU changes by re-running realpath(workdir) and realpath(/proc/self/fd/${handle.fd}) after permission approval; it returns the verified directory and still-open FileHandle.assertShell() requests action "shell" for either the quoted original command or, for writes, ${shellCommand(command,args)} <stdin> ${shellCommand(input,[])}; shellCommand() single-quotes every command/argument and escapes embedded single quotes.validateCommand() rejects empty commands, malformed Unicode, NUL bytes, more than 128 arguments, command UTF-8 size above 4,096 bytes, any argument above 16,384 UTF-8 bytes, and aggregate argument bytes above 65,536./usr/bin/grep with ["-a", "-m", String(limit), "-nE", ...(ignoreCase ? ["-i"] : []), "--", pattern], kills it after 250 ms, caps stdout at MAX_BUFFER_SIZE * 2, caps captured stderr at MAX_LINE_LENGTH, treats exit 1 as no matches, and parses line:text output./home/byk/.local/share/opencode-v2-pilot/config/opencode/plugins/followup.ts is a 158-line Promise plugin with ID "followup" that registers schedule_followup, list_followups, and cancel_followup.Error: scheduled follow-ups are disabled in subagents. A subagent must finish its task and return a result synchronously.; subagent status is determined by ctx.session.get({ sessionID }).parentID !== undefined and cached per session.schedule_followup accepts string delay and message, uses { codemode: false }, parses examples such as 30s, 2m, and 1h, treats bare numbers as seconds, rejects unparseable delays and delays below 1,000 ms, and creates an 8-character base-36 ID from Math.random().toString(36).slice(2, 10).ctx.session.prompt({ sessionID, text: \[scheduled follow-up] ${input.message}`, delivery: "steer" }); timers are unref()`βd when supported.list_followups reports only the current sessionβs timers as ${id}: in about ${seconds}s: "${message}", with remaining seconds rounded and clamped to zero.cancel_followup cancels one ID or exact ID "all"; "all" clears every current-session timer and reports the exact count, while a missing ID returns No pending follow-up with id "${input.id}".session.deleted and clears that sessionβs timers and subagent cache; unload aborts the event subscription and clears every pending session.parseDelay() accepts nonnegative decimal values and optional units ms, s, m, h, sec, secs, min, mins, hour, hours, second, seconds, minute, or minutes; multipliers are 1 for milliseconds, 1,000 for seconds/default, 60,000 for minutes, and 3,600,000 for hours, with the result rounded.