Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 20. října 2026

Procházet dokumentaci · github implementation

Tento článek zatím není přeložen. Je dostupný v angličtině.

Developer reference

github implementation

Project removal

This participant requires core 0.30.11 or later for durable removal acknowledgement and transaction-scoped active-Project admission. Update core before installing this plugin version.

Mapping writes and Project-scoped confirmation creation call tx.host.projects.requireActive(id) inside the same transaction as their final INSERT/UPDATE. Device-login finalization passes its existing bound transaction into the mapping writer, so validation completed over the network cannot commit a late mapping after deletion admission. Core's Project row lock serializes these writes with deletion across processes; account-only confirmations do not acquire a Project lock.

The plugin declares provides.projectRemoval: "services". Its idempotent registerProjectRemoved callback transactionally deletes the Project's repository mapping and action confirmations, leaving account connections, credentials and other mappings intact. Callback failure propagates to core's durable deletion workflow; missing-Project boot reconciliation is no longer a separate cleanup authority. Project actions and command credential projection reject lifecycle deleting; mapping creation rechecks after remote repository validation.

Exact text identity

Confirmation issuance and consumption in src/store.ts import hashText directly from elowen-plugin-shared/contentHash. Exact UTF-8 token bytes produce lowercase SHA-256 hex without trimming or normalization. Existing confirmation rows remain usable without migration; account/action binding, row locking, single-use consumption, expiry and random token generation are unchanged. The helper performs synchronous hashing only; domain validation and database failures remain with the store.

Migration entry

The manifest entry exports migrate(db: PluginDb<never>): Promise<void> beside register. The daemon loader always awaits it before registration for this reads: ['db'] plugin; the DB CLI calls the same export without registering tools, routes, timers or services. The export runs only the existing transactional plugin SQL through db.migrate. A failed step rolls back its SQL and ledger entry and prevents registration. Repeated calls skip applied versions. Runtime store construction never invokes migrations. Tests that bypass the loader explicitly await this export before using their fixture.

The plugin owns two things: an account's GitHub identity and the one repository mapping of each Project. It registers no agent tools. Agents publish branches, open pull requests, review and merge with git and gh in the terminal, and the Sandbox plugin signs those commands in through the github control below.

Session credential control

src/index.ts registers ctx.registerControl('github', { sessionCredential }). The control returns a Promise; Sandbox must await it before preparing a command. The Sandbox plugin calls sessionCredential({ accountUserId, projectRef }) when it prepares a terminal command and puts the returned token into that command's environment. The answer is { token, login } only when accountUserId is the account driving the current turn, projectRef names a host or managed Project, that account holds the active mapping of projectRef.projectId, its connection is connected, and the stored token is a single non-blank word. Every other case, including malformed input, an unreadable secret and an administrator asking for another account, is null; the method never writes. Database failures reject the asynchronous operation; an unreadable secret still returns null. Nothing is written to disk, so disconnecting or releasing the mapping stops future launches, while a command that already received the token keeps it until it exits.

PostgreSQL persistence

Registration awaits GitHubService.create(ctx), which obtains await ctx.db() after the host completes the entry export migrate(db) and before publishing routes, controls or lifecycle callbacks. Store methods, host project/account reads, secret operations, status projections and lifecycle cleanup are asynchronous. API handlers await them before returning ordinary JSON bodies. Callers propagate database failures. SQL-only account and mapping writers accept either the root PluginDb or a callback-scoped PluginDbTx; connection finalization passes its transaction handle so all writes use the same connection. Root handles have no transaction-only host accessor.

The plugin uses the host-selected, quoted db.schema for every object. Baseline migration 7 creates accounts, project_mappings, oauth_flows, confirmations and device_flows. Installed ledgers already record it, so it is never renumbered; future migrations continue at version 8. Timestamp columns use timestamptz; the store converts them back to epoch milliseconds for existing DTOs. The two expiry tables have explicit row_id identities. Existing historical OAuth rows remain only for the existing secret-cleanup operation; no OAuth or pull-request feature is registered.

project_mappings.project_id is unique. Mapping writes use ON CONFLICT(project_id) DO UPDATE ... WHERE current.user_id=excluded.user_id, so competing absent-row inserts cannot replace another account's mapping. The loser receives mapping_owned_by_another_account with HTTP 409. Confirmed removal includes the expected owner, repository IDs and verification timestamp in the DELETE predicate, so a changed mapping survives.

Confirmation consumption locks its row with FOR UPDATE and commits consumption before checking the action's current state. Device completion locks its flow and mapping rows, commits the account, revalidated mappings and connected state together, and rolls everything back on failure. The auth adapter records starts awaiting directory persistence so cancellation or shutdown during that await prevents the child from launching. Pending-to-completing and active-to-terminal transitions use conditional updates; late callbacks cannot revive cancelled logins or turn a committed connection into a failure. Cleanup records lock the current flow before changing its directory/error, preserving concurrent terminal reasons.

The account secret bag's existing withLock('cli-token', ...) serializes connection credential replacement and disconnect. A rejected or cancelled finalization restores the prior token before releasing that lock. Disconnect checks the expected account snapshot in its transaction before removing the connection and deactivating mappings. Tokens are bound secret values and never enter SQL, logs or confirmation payloads.

Bounded expiry

Database deletion uses core-owned drainDbRows from elowen-plugin-shared/dbBatches. It selects identities and payload sizes, commits at most 64 rows / 256 KiB per batch, processes one oversized row alone and yields between commits.

GitHubService.prune() returns a promise. The existing minute interval awaits the store's confirmation and completed-login expiry drains. Each transaction selects at most 64 identities and 256 KiB of stored payload plus row overhead, removes exactly those identities and yields through setImmediate. An oversized individual record is removed alone. The native PostgreSQL baseline creates the expiry indexes, including the partial index that excludes active logins and retained cleanup directories. It creates no pull-request creation lease table. Failed authentication-directory cleanup continues keeping its row. No confirmation is usable past its expiration, even while an expiry drain is between batches.

Device flow ownership

Internally, src/deviceFlow.ts owns request transitions, completion checks and cleanup, using the service's existing store and auth adapter. GitHubService remains the route facade and the sole owner of token migration, account-scoped secrets, mapping validation and credential commit/rollback. The device-flow owner receives only profile lookup, mapping validation and connection commit operations. Plugin shutdown marks active requests interrupted before calling the adapter's stopAll, then retries directory cleanup; it does not replace stopAll with individual cancellations.

GitHubDeviceFlow.currentFlow(userId) is the account-scoped source for pending and completing login state. GitHubService.connectionStatus, the initial start check and the fresh query after a failed flow insert use this same projection. It returns null when no active request exists and omits the private authentication directory. A concurrent insert is translated to auth_in_progress only when the fresh projection finds an active request; unrelated insertion failures propagate. The partial unique PostgreSQL index enforces one active request per account. Confirmation failures in both device login and mapping release use the stale factory in src/errors.ts, preserving state_changed and HTTP 409. Mapping-release confirmations and replacement consent consumed by auth/start are checked for stale state after consumption; generic confirmation refuses identity replacement before consumption.

Managed repository reads

The Project panel's repository rows and the mapping form's suggestions come from ctx.host.git().projectRemotes. For a managed Project the plugin passes { managed: { project, accountUserId } } and reads the canonical guest root; core runs Git in the guest through the environment provider, which is why the manifest keeps consumesControls: ['sandbox']. The plugin itself resolves no control. Repository listing and mapping detection read only the typed repository verdict and sanitized fetch/push remotes, with no snapshot fallback or status scan. One remote costs four Git processes, or four managed execution sessions, instead of five. Core 0.29.100 first shipped the typed projectRemotes reader; the plugin declares core 0.30.4. An environment refusal coded project_forbidden or account_forbidden reaches the browser as project_forbidden (HTTP 403), and a Git verdict keeps git_command_failed with its exit code and stderr.

Paired computer Projects

A paired computer is unmappable: it runs git and gh with its own credentials, so this instance holds no repository mapping for it. repositories filters computers out of the list, and asking for one by id answers computer_project_unmappable with HTTP 409 instead of reporting it as an unmapped repository. sessionCredential answers null for a { kind: 'local', projectId } ref, so a terminal command on a computer starts without a token from this instance. The Project panel recognizes the computer from the host's Project projection, renders the manifest's computersUnmapped state and issues neither the status nor the repository request.

Browser runtime contract

GitHubProjectPanel opens the existing mapping form through the host's Button variant="ghost" size="sm", matching Project icon and commit-history actions. It occupies SettingsRow.actions, keeping the row label and state readable on phones. The host owns hover, focus and disabled presentation. The external GitHub content link remains an anchor.

The GitHub account panel, account summary chip and Project repository panel register against host UI API 58. The local browser runtime uses ElowenUiRuntime['api'] and ElowenUiRuntime['utils']['interpolate'] directly. For example, the Project panel sends api(path, { method: 'POST', json: payload }) and formats connectedBy through interpolate(template, { login }). The host owns JSON serialization, authentication, cancellation and placeholder handling. Without a GitHub UI registration the host mounts no GitHub surfaces; an older host must refuse the registration instead of mounting missing capabilities.

Identity replacement and browser confirmation

Identity replacement is previewed through POST /plugins/github/api/actions/preview with type: "replace_identity". Pass its confirmationToken to POST /plugins/github/api/auth/start with replaceIdentity: true. The generic actions/confirm endpoint and GitHubService.confirm reject this action with invalid_action and HTTP 400 before consuming consent. Only auth/start consumes replacement consent and validates the current identity snapshot. Consent is account/action-bound, single-use and expires after five minutes. Without a token an existing account cannot start replacement. Stale replacement consent is consumed by auth/start and reports state_changed with HTTP 409; preview again before retrying. GitHubConnectionPanel is the browser consumer. Disconnect and mapping release consume their confirmation before execution, including stale-state rejection.

GitHubService.preview(userId, action, admin?) always persists its confirmation. There is no non-persisting preview mode. Browser routes pass the request's administrator status. The action set is closed: disconnect, replace_identity and remove_mapping.

The plugin-local useGitHubConfirmation hook in web-src/GitHubConnectionPanel.tsx owns pending preview state, concurrent-preview suppression and fresh-token retry for both GitHubConnectionPanel and GitHubProjectPanel. Call requestPreview(action) for a user-requested preview and retryPreview(action) after a failed operation has consumed its token. No pending preview means no confirmation dialog; mounting the hook sends no request. Each retry requests one new preview. Device-login state and replacement auth/start remain in the connection panel. The Project panel refreshes its repository query on HTTP 409 instead of retrying against stale data.

GitHubConnectionPanel takes no change callback. Connection completion, cancellation and disconnection invalidate the shared plugin/github/status query. The linked-account row and GitHubAccountChip observe that same query; a disconnected account has no summary chip.