22-ai-00/dsh-enhanced--plugins-assistant-actions2

@dsh-enhanced/assistant-actions

Owner-authorized GitHub action facade with embedded compatibility and an external Unix broker.

包名
@dsh-enhanced/assistant-actions
版本
0.1.28
许可证
MIT
最近更新
2026年9月12日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:22-ai-00/dsh-enhanced#bb3fb924f1bdb946901614b8b848ac3a1d60c642&path:plugins/assistant-actions

@dsh-enhanced/assistant-actions

Experimental finite, owner-authorized GitHub action bundle. Its Cordis-facing service preserves the existing tool and Host API, while deployment selects one of two explicit backends: legacy embedded-compat, or the versioned external-unix-v1 broker. Embedded mode keeps the existing trusted-Host Keychain/ledger/HTTPS path. External mode sends signed, bounded requests to a separately started dsh-actions-broker process, which owns the credential, authoritative grants, journal and GitHub HTTPS connection. Neither mode gives a credential to the model or isolated worker. An empty grant set exposes no usable GitHub authority.

Installation and compatibility

Install this bundle with matching checkout/release versions of assistant-policy, assistant-delivery, and assistant-isolation when shell execution is needed. Embedded compatibility mode additionally needs credentials-keychain; external mode deliberately does not ask the Host Keychain for the GitHub token. Tool registration also needs native Agents and Tools. The bundle requires DSH 0.1.2-rc.1, Cordis ^4.0.1, Node ^22.19.0 || >=24, Linux and POSIX private-file ownership semantics for the external Unix sockets, and the Policy preauthorization/evaluateAgent methods introduced with this broker. The package dependency on koffi supplies the broker's fixed Linux getsockopt(SO_PEERCRED) binding. Optional peer metadata controls package installation only; it does not make runtime authentication optional.

dsh plugin --profile web add @dsh-enhanced/assistant-actions
dsh --profile web --dump-config

Use a current owner record and an existing non-production branch for first deployment. The following is the legacy embedded-compatible shape; configuration grants contain no token:

stateRoot: /absolute/private/assistant-actions
grants:
  - id: repo-fix
    revision: 1
    principalDigest: 
    principalRecordId: 
    principalVersion: 1
    workspace: /absolute/project
    agentPreset: primary
    repository: owner/repository
    branch: automation/fix
    paths: [src/example.ts, tests/example.spec.ts]
    credentialHandle: github-repository-token
    expiresAt: 
    maxActions: 10
    maxTotalBytes: 1048576
    rollback: # omitted means that no forward commit can be compensated
      allowRollback: true
      budgetId: github-compensation
      maxActions: 2
      maxTotalBytes: 1048576
    repoWorkflow: # omitted means branch/PR writes are unavailable
      baseBranch: main
      allowBranchCreate: true
      allowPullRequest: true

In embedded mode the Keychain handle must allow consumer dsh-enhanced-assistant-actions, purpose github.commit, and leases up to 30 seconds. A rollback-enabled grant also requires purpose github.compensate; commit permission does not imply compensation permission. Reuse the Keychain provider appropriate for the environment and never place a token in this YAML. Policy must separately allow the exact agent/owner execute resource { kind: tool, id: "action:github:repo-fix" }, native tool execution of action_github_commit, and background credential.use for the chosen handle. Any Policy deny or emergency stop remains authoritative. Explicit capability grants skip only Policy's generic risk prompt for this exact broker definition; they do not disable other approval providers or arbitrary tool checks. Policy budget evaluation during polling is read-only; one authorization with a stable action key is charged before credential retrieval and dispatch. Failed credential acquisition does not refund that budget.

External Unix broker

external-unix-v1 is opt-in and fail-closed. The Host config names the canonical action Unix socket, pinned broker identity/public key, client signing key, expected action-socket ownership/mode, protocol timeouts and a minimum broker generation. The separately managed daemon reads its own strict configuration and private state. That daemon configuration—not the Host mirror—is authoritative for grant revision, owner/binding generation, repository, branch, paths, limits, allowed operations, allowed inspect kinds and the credential locator. External mode accepts expected-head commit, repository, branch, grant-path-scoped file, and explicitly authorized pull-request, checks, and reviews inspection. PR-related inspection requires a grant-fixed destination.baseBranch, distinct from the head branch, plus an exact request PR number. Both repositories and both refs are verified before any observation is returned. External verified delivery queues independently accepted artifacts for a broker commit followed by an explicitly authorized PR, and supports fresh repository-goal readback. Branch creation and compensation remain unsupported in external mode and never fall back to embedded execution. Direct model PR creation remains embedded-only; external PR creation is used by the trusted verified-delivery handoff.

The Host-side configuration uses the exact read-only grant projection below. grants must be empty in external mode. externalGrants.allowedOperations and externalGrants.allowedInspectKinds let the Host conservatively limit discovery and preauthorization before contacting the broker; the latter accepts all six inspect kinds, with destination.baseBranch required for PR-related kinds. Existing grants without the new kinds retain their original canonical digest and authority; enabling these reads requires a new broker-issued grant and matching projection. The projection still contains no credential locator, clientKeyId, client identity, policy/emergency epochs, or rollback authority. Optional verifiedDelivery metadata binds the Host owner route, finite scheduling budget and acceptance mode to the broker-issued grant; it is not a credential or a broker-side artifact verifier. The broker-owned grant remains authoritative, so matching projection fields can only narrow Host exposure and cannot expand broker authority. Its grantDigest must match the authoritative daemon grant; source binds the input classification and provenance. maxCostUnits mirrors the grant's finite cumulative github-api-units budget (one unit for a commit, PR creation or repository/branch/file/PR inspection; two units for checks or reviews, which first resolve the exact PR head); it is not a currency or provider-billing guarantee.

stateRoot: /absolute/private/assistant-actions-host
broker:
  mode: external-unix-v1
  actionSocketPath: /run/dsh-actions-broker/action/broker.sock
  brokerId: github-primary
  brokerPublicKeyPath: /absolute/private/host/broker-public.pem
  clientKeyId: host-primary
  clientSigningKeyPath: /absolute/private/host/client-private.pem
  clientInstanceId: dsh-host-primary
  clientGeneration: 1
  requestTimeoutMs: 30000
  helloTtlMs: 30000
  expectedSocketUid: 

  expectedSocketGid: 
  expectedSocketMode: 432 # 0660
  expectedSocketParentUid: 

  expectedSocketParentGid: 
  expectedSocketParentMode: 488 # 0750; use 448 (0700) for same-UID-only access
  expectedBrokerPeerUid: 

  expectedBrokerPeerGid: 

  minimumBrokerGeneration: 1
grants: []
externalGrants:
  - id: repo-fix
    revision: 1
    grantDigest: 
    owner:
      principalDigest: 
      principalRecordId: 
      principalVersion: 1
      workspace: /absolute/project
      preset: primary
      bindingId: 
      bindingVersion: 1
      bindingGeneration: 1
    sessionId: 
    destination:
      classification: github-repository
      repository: owner/repository
      branch: automation/fix
      paths: [src/example.ts, tests/example.spec.ts]
    expiresAt: 
    maxActions: 10
    maxTotalBytes: 1048576
    allowedOperations: [commit, inspect]
    allowedInspectKinds: [repository, branch, file]
    source:
      classification: internal
      provenanceDigest: 
    maxCostUnits: 10

The Host verifies the action socket's parent and socket metadata against all six expectedSocket* fields before and after connecting. After connect and before reading the server hello, its package-provided Linux SO_PEERCRED inspector must also report the exact expectedBrokerPeerUid/expectedBrokerPeerGid; this prevents a pathname-compatible process under another identity from serving the connection. For a separate Host UID, provision a dedicated group (or an equivalent ACL), make the Host a member, and pair a traversable group-owned parent such as 0750 with an action socket mode such as 0660; both Host and daemon configs must pin those deployed UID/GID/mode values. A same-UID-only layout can instead keep the parent 0700 and socket 0600. These filesystem permissions only make the endpoint reachable and do not replace signed protocol authentication or either side's live peer-credential check.

The wire protocol is assistant-actions/github-broker/v1: a four-byte big-endian length followed by canonical UTF-8 JSON, with an 8 MiB request ceiling and 2 MiB response ceiling. A fresh connection receives a short-lived signed challenge; client requests and broker receipts use pinned Ed25519 keys and bind broker/client generations, request and payload digests, owner lineage, Session, grant revision, destination, deadline, Policy/emergency epochs and finite budget. Unknown fields, non-canonical JSON, invalid signatures, stale generations, oversized/truncated/trailing frames, socket identity changes, timeouts and unavailable daemons are rejected. Action requests are accepted only on the action socket; signed operator requests are accepted only on the admin socket. The client performs no automatic retry. The server, Host client and administrator must use three pairwise-distinct Ed25519 key pairs and distinct client/admin key identifiers. Equal key material is rejected before either listener is opened, even when different files contain it.

Start and administer the daemon explicitly with the package executable; the Cordis plugin never spawns it. serve and the administrative commands deliberately use separate configuration files:

dsh-actions-broker serve /absolute/private/broker-serve.json
dsh-actions-broker status /absolute/private/broker-admin.json
dsh-actions-broker stop /absolute/private/broker-admin.json  operator-request
dsh-actions-broker resume /absolute/private/broker-admin.json  
dsh-actions-broker revoke /absolute/private/broker-admin.json     
  operator-request

The relevant transport/admission fields in the serve configuration are separate for the two listeners (the full file also contains the broker keys, state, credentials, grants and policy fields):

{
  "actionSocketPath": "/run/dsh-actions-broker/action/broker.sock",
  "adminSocketPath": "/run/dsh-actions-broker/admin/broker.sock",
  "expectedClientPeerUid": 1001,
  "expectedClientPeerGid": 2001,
  "expectedAdminPeerUid": 1002,
  "expectedAdminPeerGid": 2002,
  "expectedActionSocketUid": 1000,
  "expectedActionSocketGid": 2001,
  "expectedActionParentMode": 488,
  "expectedActionSocketMode": 432,
  "expectedAdminSocketUid": 1000,
  "expectedAdminSocketGid": 2002,
  "expectedAdminParentMode": 488,
  "expectedAdminSocketMode": 432,
  "maxActionConnections": 32,
  "maxAdminConnections": 4,
  "maxConcurrentRequests": 2
}

Here the decimal modes are 0750, 0660, 0750, and 0660, respectively. Choose UID/GID values for the actual deployment rather than copying these placeholders. The operator configuration names only the admin endpoint:

{
  "adminSocketPath": "/run/dsh-actions-broker/admin/broker.sock",
  "brokerId": "github-primary",
  "minimumBrokerGeneration": 1,
  "brokerPublicKeyPath": "/absolute/private/operator/broker-public.pem",
  "adminKeyId": "operator-primary",
  "adminPrivateKeyPath": "/absolute/private/operator/admin-private.pem",
  "adminInstanceId": "operator-primary",
  "adminGeneration": 1,
  "expectedAdminSocketUid": 1000,
  "expectedAdminSocketGid": 2002,
  "expectedAdminSocketMode": 432,
  "expectedAdminParentUid": 1000,
  "expectedAdminParentGid": 2002,
  "expectedAdminParentMode": 488,
  "expectedBrokerPeerUid": 1000,
  "expectedBrokerPeerGid": 1000,
  "requestTimeoutMs": 30000,
  "helloTtlMs": 30000
}

Both configuration paths must be canonical absolute paths to owner-owned, 0600, nonsymlink regular files containing only the exact JSON keys for their command class. The serve config has distinct actionSocketPath and adminSocketPath values. It independently pins expectedClientPeerUid/expectedClientPeerGid and expectedAdminPeerUid/expectedAdminPeerGid; action/admin socket ownership uses the respective expectedActionSocketUid/expectedActionSocketGid and expectedAdminSocketUid/expectedAdminSocketGid; and the two endpoints have separate expectedActionParentMode/expectedActionSocketMode and expectedAdminParentMode/expectedAdminSocketMode. maxActionConnections and maxAdminConnections bound the listeners independently, while maxConcurrentRequests bounds action execution only. The serve config contains adminKeyId and adminPublicKeyPath, but never the administrative private key.

The operator config used by status, stop, resume, and revoke contains only adminSocketPath—never actionSocketPath—plus the pinned broker public identity, admin signing identity, expected admin parent/socket ownership/mode, expectedBrokerPeerUid/expectedBrokerPeerGid, and protocol limits. In particular it contains adminKeyId, adminPrivateKeyPath, adminInstanceId, and adminGeneration; it does not contain daemon credentials, client keys or the server private key. Like the Host client, the operator client checks pathname identity, then verifies the connected broker's kernel peer UID/GID before reading the signed hello.

stop also accepts security-response or maintenance; revoke also accepts security-response, grant-replaced, or grant-expired. Administrative requests use that separately pinned signing identity and the dedicated bounded admin socket rather than opening SQLite directly. An action connection cannot carry an admin request, and action connection/request saturation does not consume the admin listener's reserved connection capacity. The stop command is a durable emergency admission stop: it rejects new actions, terminalizes/aborts affected in-flight work and advances the emergency epoch, but it does not exit the daemon or close the admin control plane; use the service manager, SIGTERM, or SIGINT to stop the process. resume advances the durable emergency epoch again, so it cannot revive an old request. Before binding either socket, serve requires Linux and the package-provided koffi native binding for getsockopt(SO_PEERCRED) to be available. Unsupported/non-Linux hosts or an unavailable native binding fail closed before bind; accepted connections must then match the role-specific peer UID/GID. Pathname ownership alone is not treated as peer authentication. Secrets and private-key bytes must not be supplied through argv, environment, Host config, tool input or IPC.

Deployments may make the action parent/socket group-accessible (for example through a dedicated group or an equivalent ACL) so a Host running under an independent UID can connect, while still pinning the resulting action parent/socket metadata and the Host peer UID/GID. The admin parent/socket should remain private to the broker/operator trust boundary. These access controls