Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e583ab55b | ||
|
|
f4b5f896b4 | ||
|
|
e2a6df5900 | ||
|
|
e471dbae13 | ||
|
|
9f443e982b | ||
|
|
4668061b19 | ||
|
|
36d08868c9 | ||
|
|
17c5890961 |
No files matched your search
@@ -37,10 +37,10 @@ docs/architecture/
|
|||||||
|
|
||||||
The README is the entry point. It contains:
|
The README is the entry point. It contains:
|
||||||
|
|
||||||
1. **Current State** — what phase the project is in, what's implemented
|
1. **Current State**: what phase the project is in, what's implemented
|
||||||
2. **Architecture Documents** — table linking to each spec doc with status
|
2. **Architecture Documents**: a table linking to each spec doc with status
|
||||||
3. **ADR Table** — every decision with number, title, and status
|
3. **ADR Table**: every decision with number, title, and status
|
||||||
4. **Open Questions** — link to `open-questions.md`
|
4. **Open Questions**: a link to `open-questions.md`
|
||||||
|
|
||||||
### Spec Documents
|
### Spec Documents
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ Accepted | Proposed | Deprecated | Superseded
|
|||||||
(Links to related specs and ADRs)
|
(Links to related specs and ADRs)
|
||||||
```
|
```
|
||||||
|
|
||||||
ADR numbering starts at 001 within each project. ADRs are stable — once
|
ADR numbering starts at 001 within each project. ADRs are stable; once
|
||||||
Accepted, they don't revert. If a decision is superseded, create a new ADR and
|
Accepted, they don't revert. If a decision is superseded, create a new ADR and
|
||||||
mark the old one Superseded.
|
mark the old one Superseded.
|
||||||
|
|
||||||
@@ -97,12 +97,12 @@ don't need ADRs.
|
|||||||
`open-questions.md` contains all unresolved questions across all spec documents,
|
`open-questions.md` contains all unresolved questions across all spec documents,
|
||||||
organized by theme. Each question has:
|
organized by theme. Each question has:
|
||||||
|
|
||||||
- **OQ-ID** (OQ-01, OQ-02, ...) — stable reference
|
- **OQ-ID** (OQ-01, OQ-02, ...), the stable reference
|
||||||
- **Origin** — which spec doc(s) the question appeared in
|
- **Origin**: which spec doc(s) the question appeared in
|
||||||
- **Status** — open, resolved, partially resolved
|
- **Status**: open, resolved, partially resolved
|
||||||
- **Priority** — high, medium, low
|
- **Priority**: high, medium, low
|
||||||
- **Resolution** — when resolved, what was decided and which ADR addresses it
|
- **Resolution**: when resolved, what was decided and which ADR addresses it
|
||||||
- **Cross-references** — related OQs and ADRs
|
- **Cross-references**: related OQs and ADRs
|
||||||
|
|
||||||
Spec documents reference OQs by number, not by repeating the question inline.
|
Spec documents reference OQs by number, not by repeating the question inline.
|
||||||
When an OQ is resolved, leave a strikethrough + resolution note in the spec
|
When an OQ is resolved, leave a strikethrough + resolution note in the spec
|
||||||
@@ -136,7 +136,7 @@ Before writing architecture:
|
|||||||
- Understand the problem domain
|
- Understand the problem domain
|
||||||
- Identify constraints and quality attributes
|
- Identify constraints and quality attributes
|
||||||
- Research similar systems if needed
|
- Research similar systems if needed
|
||||||
- Read downstream consumer architecture — if the project is a library, understand
|
- Read downstream consumer architecture; if the project is a library, understand
|
||||||
what consumers need
|
what consumers need
|
||||||
|
|
||||||
### 2. Identify Documentation Scope
|
### 2. Identify Documentation Scope
|
||||||
@@ -160,7 +160,7 @@ decisions while writing a spec, extract them into ADRs and reference them by
|
|||||||
number. As you identify open questions, add them to `open-questions.md` and
|
number. As you identify open questions, add them to `open-questions.md` and
|
||||||
reference them by OQ-ID.
|
reference them by OQ-ID.
|
||||||
|
|
||||||
Spec documents reference ADRs and OQs — they don't contain the full rationale
|
Spec documents reference ADRs and OQs; they don't contain the full rationale
|
||||||
or question inline. This keeps specs focused on WHAT, ADRs focused on WHY, and
|
or question inline. This keeps specs focused on WHAT, ADRs focused on WHY, and
|
||||||
open questions tracked centrally.
|
open questions tracked centrally.
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ Before requesting external review:
|
|||||||
blocking condition (for `deferred(scope)`) or investigation target
|
blocking condition (for `deferred(scope)`) or investigation target
|
||||||
(for `deferred(unclear)`) isn't a *prerequisite* of the thing you're
|
(for `deferred(unclear)`) isn't a *prerequisite* of the thing you're
|
||||||
deferring. If the blocker needs what you're deferring, you have a
|
deferring. If the blocker needs what you're deferring, you have a
|
||||||
prerequisite inversion — either make the decision now (the pieces
|
prerequisite inversion: either make the decision now (the pieces
|
||||||
exist) or reframe honestly (the shape isn't clear, here's what would
|
exist) or reframe honestly (the shape isn't clear, here's what would
|
||||||
make it clear).
|
make it clear).
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ When you encounter a decision that genuinely can't be made:
|
|||||||
1. Mark the OQ as `deferred(scope)` with a concrete blocking condition
|
1. Mark the OQ as `deferred(scope)` with a concrete blocking condition
|
||||||
2. Create a blocker task in `tasks/architecture/` naming the dependency
|
2. Create a blocker task in `tasks/architecture/` naming the dependency
|
||||||
(frontmatter schema: §"Task files you write" under Safe Exit below)
|
(frontmatter schema: §"Task files you write" under Safe Exit below)
|
||||||
3. Continue to decisions that *can* be made — do not stall on one question
|
3. Continue to decisions that *can* be made; do not stall on one question
|
||||||
|
|
||||||
### 6. Request Architecture Review
|
### 6. Request Architecture Review
|
||||||
|
|
||||||
@@ -217,12 +217,12 @@ task(
|
|||||||
|
|
||||||
Address feedback:
|
Address feedback:
|
||||||
|
|
||||||
- **Critical**: Must fix before stabilization — inline decisions not
|
- **Critical**: Must fix before stabilization; inline decisions not
|
||||||
extracted, ADR references that point to nonexistent files, undefined
|
extracted, ADR references that point to nonexistent files, undefined
|
||||||
terms, circular deferrals
|
terms, circular deferrals
|
||||||
- **Warning**: Should fix — missing cross-references, documents approaching
|
- **Warning**: Should fix (missing cross-references, documents approaching
|
||||||
split threshold
|
split threshold
|
||||||
- **Suggestion**: Consider — minor clarity improvements
|
- **Suggestion**: Consider (minor clarity improvements
|
||||||
|
|
||||||
Iterate until zero critical issues.
|
Iterate until zero critical issues.
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ last_updated: 2026-05-29
|
|||||||
answer is resolved, not left "open" with hedging language like "v1 default"
|
answer is resolved, not left "open" with hedging language like "v1 default"
|
||||||
or "can be revisited later." If the decision is made, mark it resolved. If
|
or "can be revisited later." If the decision is made, mark it resolved. If
|
||||||
the decision genuinely can't be made yet (the use case isn't concrete,
|
the decision genuinely can't be made yet (the use case isn't concrete,
|
||||||
the options aren't clear), mark it `deferred(scope)` — see Safe Exit below.
|
the options aren't clear), mark it `deferred(scope)` (see Safe Exit below).
|
||||||
The architect's job is to make architecture decisions that *can* be made
|
The architect's job is to make architecture decisions that *can* be made
|
||||||
and to clearly identify which decisions *can't* be made yet and why.
|
and to clearly identify which decisions *can't* be made yet and why.
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ last_updated: 2026-05-29
|
|||||||
|
|
||||||
Door type classifies **reversal cost** (one-way vs two-way), not
|
Door type classifies **reversal cost** (one-way vs two-way), not
|
||||||
urgency. A two-way door is a decision you make now and can revert
|
urgency. A two-way door is a decision you make now and can revert
|
||||||
later — not a decision to defer. Using "it's a two-way door" as a
|
later, not a decision to defer. Using "it's a two-way door" as a
|
||||||
reason to leave a decision unmade conflates reversal cost with
|
reason to leave a decision unmade conflates reversal cost with
|
||||||
decision-making. See ADR-009 §"What this framework is NOT" for the
|
decision-making. See ADR-009 §"What this framework is NOT" for the
|
||||||
full rationale.
|
full rationale.
|
||||||
@@ -282,17 +282,17 @@ Architecture decisions are the architect's, regardless of door type.
|
|||||||
The implementation agent makes implementation decisions (variable
|
The implementation agent makes implementation decisions (variable
|
||||||
names, loop order, which library to use for a concrete task). If a
|
names, loop order, which library to use for a concrete task). If a
|
||||||
decision affects the system's structure, constraints, or API surface,
|
decision affects the system's structure, constraints, or API surface,
|
||||||
it's an architecture decision — even if it's a two-way door.
|
it's an architecture decision, even if it's a two-way door.
|
||||||
|
|
||||||
## Anti-Patterns to Avoid
|
## Anti-Patterns to Avoid
|
||||||
|
|
||||||
1. **Inline decisions**: DD1, D3, SE2 etc. in spec docs — extract to ADRs
|
1. **Inline decisions**: DD1, D3, SE2 etc. in spec docs; extract to ADRs
|
||||||
2. **Inline open questions**: Scattered per-doc "Open Questions" sections —
|
2. **Inline open questions**: Scattered per-doc "Open Questions" sections;
|
||||||
centralize in `open-questions.md`
|
centralize in `open-questions.md`
|
||||||
3. **Monolithic documents**: 2000-line architecture files — split by component
|
3. **Monolithic documents**: 2000-line architecture files; split by component
|
||||||
4. **Duplication across documents**: Cross-reference ADRs and OQs, don't
|
4. **Duplication across documents**: Cross-reference ADRs and OQs, don't
|
||||||
copy-paste rationale
|
copy-paste rationale
|
||||||
5. **Historical comparison**: "Here's what the old code did" — specs describe
|
5. **Historical comparison**: "Here's what the old code did"; specs describe
|
||||||
the current design, not the transition from before
|
the current design, not the transition from before
|
||||||
6. **Missing ADR for a visible choice**: If a reader would ask "why X over Y?",
|
6. **Missing ADR for a visible choice**: If a reader would ask "why X over Y?",
|
||||||
write an ADR
|
write an ADR
|
||||||
@@ -311,7 +311,7 @@ reasoning to see its own circular hedges; a fresh context catches them.
|
|||||||
## Safe Exit: Deferred Decisions
|
## Safe Exit: Deferred Decisions
|
||||||
|
|
||||||
When a decision can't be made yet, the architect has a Safe Exit path.
|
When a decision can't be made yet, the architect has a Safe Exit path.
|
||||||
This is not a failure — it's scope management. The architect's job is
|
This is not a failure; it's scope management. The architect's job is
|
||||||
to make decisions that *can* be made and to clearly identify which
|
to make decisions that *can* be made and to clearly identify which
|
||||||
decisions *can't* be made yet and why.
|
decisions *can't* be made yet and why.
|
||||||
|
|
||||||
@@ -319,10 +319,10 @@ There are two kinds of deferral. The distinction matters because they
|
|||||||
have different resolution paths, and confusing them is a source of
|
have different resolution paths, and confusing them is a source of
|
||||||
circular reasoning.
|
circular reasoning.
|
||||||
|
|
||||||
### `deferred(scope)` — the information is genuinely missing
|
### `deferred(scope)`: the information is genuinely missing
|
||||||
|
|
||||||
The decision can't be made because something the decision depends on
|
The decision can't be made because something the decision depends on
|
||||||
doesn't exist yet. Resolution is *waiting* — for a crate spec, a POC
|
doesn't exist yet. Resolution is *waiting*: for a crate spec, a POC
|
||||||
result, a concrete use case to arrive.
|
result, a concrete use case to arrive.
|
||||||
|
|
||||||
A decision should be `deferred(scope)` when:
|
A decision should be `deferred(scope)` when:
|
||||||
@@ -337,11 +337,11 @@ A decision should be `deferred(scope)` when:
|
|||||||
current scope is core + call crates; this question is about the agent
|
current scope is core + call crates; this question is about the agent
|
||||||
crate")
|
crate")
|
||||||
|
|
||||||
### `deferred(unclear)` — the pieces exist but the shape isn't clear
|
### `deferred(unclear)`: the pieces exist but the shape isn't clear
|
||||||
|
|
||||||
The pieces of the decision exist (decided in other ADRs, existing
|
The pieces of the decision exist (decided in other ADRs, existing
|
||||||
types, existing patterns) but the composition — how they fit together
|
types, existing patterns) but how the composition fits together into a
|
||||||
into a coherent shape — isn't clear yet. Resolution is *investigation*,
|
coherent shape isn't clear yet. Resolution is *investigation*,
|
||||||
not waiting: work through example use cases, maybe build a POC, maybe
|
not waiting: work through example use cases, maybe build a POC, maybe
|
||||||
just think through the composition until the shape surfaces.
|
just think through the composition until the shape surfaces.
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ pieces can be decided (verifier selection, crypto provider, fingerprint
|
|||||||
normalization) while the shape they compose into (the client config
|
normalization) while the shape they compose into (the client config
|
||||||
type) is still unclear. Forcing a decision in that state produces a
|
type) is still unclear. Forcing a decision in that state produces a
|
||||||
guess; forcing a `deferred(scope)` produces a false deferral (the
|
guess; forcing a `deferred(scope)` produces a false deferral (the
|
||||||
information isn't missing — it's un-synthesized). `deferred(unclear)`
|
information isn't missing; it's un-synthesized). `deferred(unclear)`
|
||||||
is the honest state: "I can see the pieces but I can't see the shape
|
is the honest state: "I can see the pieces but I can't see the shape
|
||||||
yet, and I need to work through examples to see it."
|
yet, and I need to work through examples to see it."
|
||||||
|
|
||||||
@@ -364,28 +364,28 @@ A decision should be `deferred(unclear)` when:
|
|||||||
- Resolution requires *work* (thinking through examples, building a
|
- Resolution requires *work* (thinking through examples, building a
|
||||||
POC), not *waiting* (for a spec or use case to arrive)
|
POC), not *waiting* (for a spec or use case to arrive)
|
||||||
- The architect can articulate what investigation would help ("work
|
- The architect can articulate what investigation would help ("work
|
||||||
through 2+ example outbound-dial use cases") — if you can't, that's a
|
through 2+ example outbound-dial use cases"); if you can't, that's a
|
||||||
signal the deferral might be circular
|
signal the deferral might be circular
|
||||||
|
|
||||||
### How to Defer
|
### How to Defer
|
||||||
|
|
||||||
1. **Mark the OQ as `deferred(scope)` or `deferred(unclear)`** — not
|
1. **Mark the OQ as `deferred(scope)` or `deferred(unclear)`**, not
|
||||||
`open` (implies it should be resolved now) and not `resolved`
|
`open` (implies it should be resolved now) and not `resolved`
|
||||||
(implies it's decided).
|
(implies it's decided).
|
||||||
2. **State the blocking condition** (`deferred(scope)`) or
|
2. **State the blocking condition** (`deferred(scope)`) or
|
||||||
**investigation target** (`deferred(unclear)`) — what specific thing
|
**investigation target** (`deferred(unclear)`): what specific thing
|
||||||
would unblock this? Be concrete: "blocked on: the alkfs sftp-mapping
|
would unblock this? Be concrete: "blocked on: the alkfs sftp-mapping
|
||||||
spec exists" or "investigation: work through 2+ example git-over-ssh
|
spec exists" or "investigation: work through 2+ example git-over-ssh
|
||||||
use cases (clone and push) to see how identity + auth + the
|
use cases (clone and push) to see how identity + auth + the
|
||||||
exec/subsystem channel mapping compose."
|
exec/subsystem channel mapping compose."
|
||||||
3. **State the impacts** — what does this block downstream? Be
|
3. **State the impacts**: what does this block downstream? Be
|
||||||
specific: "blocks the http endpoint because receive-pack needs
|
specific: "blocks the http endpoint because receive-pack needs
|
||||||
unbuffered POST ingestion" not "blocks the http crate." This is the
|
unbuffered POST ingestion" not "blocks the http crate." This is the
|
||||||
triage signal that
|
triage signal that
|
||||||
makes the deferral's urgency visible. If the impact is significant,
|
makes the deferral's urgency visible. If the impact is significant,
|
||||||
the deferral needs to be addressed soon; if it's a future feature,
|
the deferral needs to be addressed soon; if it's a future feature,
|
||||||
it can wait.
|
it can wait.
|
||||||
4. **Move on** — the architect continues to decisions that *can* be
|
4. **Move on**: the architect continues to decisions that *can* be
|
||||||
made. Deferred decisions are not failures; they're the input to the
|
made. Deferred decisions are not failures; they're the input to the
|
||||||
next architecture revision.
|
next architecture revision.
|
||||||
|
|
||||||
@@ -398,10 +398,10 @@ A decision should be `deferred(unclear)` when:
|
|||||||
- **Status**: deferred(scope) | deferred(unclear)
|
- **Status**: deferred(scope) | deferred(unclear)
|
||||||
- **Door type**: <one-way | two-way>
|
- **Door type**: <one-way | two-way>
|
||||||
- **Priority**: <high | medium | low>
|
- **Priority**: <high | medium | low>
|
||||||
- **Impacts**: <what this blocks downstream — be specific>
|
- **Impacts**: <what this blocks downstream; be specific>
|
||||||
- **Blocked on**: <concrete dependency> (for deferred(scope))
|
- **Blocked on**: <concrete dependency> (for deferred(scope))
|
||||||
**Investigation**: <what work would make the shape clear> (for deferred(unclear))
|
**Investigation**: <what work would make the shape clear> (for deferred(unclear))
|
||||||
- **Resolution**: Not yet decidable. <Why — either the information
|
- **Resolution**: Not yet decidable. <Why: either the information
|
||||||
doesn't exist yet (deferred(scope)) or the pieces exist but the
|
doesn't exist yet (deferred(scope)) or the pieces exist but the
|
||||||
composition isn't clear (deferred(unclear), cite the pieces).>
|
composition isn't clear (deferred(unclear), cite the pieces).>
|
||||||
- **Cross-references**: OQ-NN, ADR-NNN
|
- **Cross-references**: OQ-NN, ADR-NNN
|
||||||
@@ -410,14 +410,15 @@ A decision should be `deferred(unclear)` when:
|
|||||||
### Task files you write (blocker/OQ trackers in `tasks/architecture/`)
|
### Task files you write (blocker/OQ trackers in `tasks/architecture/`)
|
||||||
|
|
||||||
When you file or edit a task under `tasks/`, you follow the taskgraph
|
When you file or edit a task under `tasks/`, you follow the taskgraph
|
||||||
format — the same closed schema the decomposer uses. Frontmatter
|
format, the same closed schema the decomposer uses. Frontmatter
|
||||||
(taskgraph 0.1.3; invalid variants fail `taskgraph validate`):
|
(taskgraph 0.1.3; invalid variants fail `taskgraph validate`):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
id: architecture/<slug> # required
|
id: architecture/<slug> # required
|
||||||
name: <human task name> # required
|
name: <human task name> # required
|
||||||
status: pending # pending | in-progress | completed | failed | blocked — never `done`
|
status: pending # pending | in-progress | completed | failed;
|
||||||
|
# never `done`
|
||||||
depends_on: [] # task ids; the graph is the dependency record
|
depends_on: [] # task ids; the graph is the dependency record
|
||||||
scope: narrow # single | narrow | moderate | broad | system
|
scope: narrow # single | narrow | moderate | broad | system
|
||||||
risk: low # trivial | low | medium | high | critical
|
risk: low # trivial | low | medium | high | critical
|
||||||
@@ -427,7 +428,7 @@ tags: [deferred-oq, ...] # free-form
|
|||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Every file under `tasks/` carries a complete frontmatter block** —
|
- **Every file under `tasks/` carries a complete frontmatter block**;
|
||||||
a file without frontmatter is *silently skipped* by `taskgraph
|
a file without frontmatter is *silently skipped* by `taskgraph
|
||||||
validate`/`list` and every graph command: no error, the task simply
|
validate`/`list` and every graph command: no error, the task simply
|
||||||
doesn't exist as far as the graph is concerned. Prose-only trackers
|
doesn't exist as far as the graph is concerned. Prose-only trackers
|
||||||
@@ -439,7 +440,7 @@ tags: [deferred-oq, ...] # free-form
|
|||||||
§"Architecture work and the `level` field" (ADR/spec = `implementation`,
|
§"Architecture work and the `level` field" (ADR/spec = `implementation`,
|
||||||
POC/validation = `research`, decomposition pass = `decomposition`,
|
POC/validation = `research`, decomposition pass = `decomposition`,
|
||||||
review = `review`). Deferred-OQ trackers are usually `research`.
|
review = `review`). Deferred-OQ trackers are usually `research`.
|
||||||
- **`taskgraph validate` is the gate** — run it after writing or
|
- **`taskgraph validate` is the gate**; run it after writing or
|
||||||
editing anything under `tasks/`. Semantics of the categorical fields
|
editing anything under `tasks/`. Semantics of the categorical fields
|
||||||
live in `docs/sdd_process.md` §"Categorical Estimates".
|
live in `docs/sdd_process.md` §"Categorical Estimates".
|
||||||
|
|
||||||
@@ -448,12 +449,12 @@ tags: [deferred-oq, ...] # free-form
|
|||||||
- Do not mark a deferred decision as `resolved` with caveats. "Resolved
|
- Do not mark a deferred decision as `resolved` with caveats. "Resolved
|
||||||
with an escape hatch" is hedging.
|
with an escape hatch" is hedging.
|
||||||
- Do not leave a deferred decision as `open` without a blocking
|
- Do not leave a deferred decision as `open` without a blocking
|
||||||
condition. "Open" means "needs to be resolved now" — if it can't be
|
condition. "Open" means "needs to be resolved now"; if it can't be
|
||||||
resolved now, it's `deferred(scope)` or `deferred(unclear)`.
|
resolved now, it's `deferred(scope)` or `deferred(unclear)`.
|
||||||
- Do not confuse the two deferral kinds. If the information is missing,
|
- Do not confuse the two deferral kinds. If the information is missing,
|
||||||
it's `deferred(scope)`. If the information exists but the shape isn't
|
it's `deferred(scope)`. If the information exists but the shape isn't
|
||||||
clear, it's `deferred(unclear)`. Confusing them produces circular
|
clear, it's `deferred(unclear)`. Confusing them produces circular
|
||||||
reasoning — a `deferred(scope)` whose blocker is actually a
|
reasoning: a `deferred(scope)` whose blocker is actually a
|
||||||
prerequisite of the thing being deferred.
|
prerequisite of the thing being deferred.
|
||||||
|
|
||||||
## When to Redirect
|
## When to Redirect
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ Check coverage of:
|
|||||||
|
|
||||||
#### F. Decision Quality and Deferral Honesty
|
#### F. Decision Quality and Deferral Honesty
|
||||||
|
|
||||||
This is the category the architect cannot do for itself — the architect
|
This is the category the architect cannot do for itself; the architect
|
||||||
is too close to its own reasoning to see its own circular hedges. A
|
is too close to its own reasoning to see its own circular hedges. A
|
||||||
fresh context can see the whole picture and spot reasoning that folds
|
fresh context can see the whole picture and spot reasoning that folds
|
||||||
back on itself. This is often the highest-value category on projects
|
back on itself. This is often the highest-value category on projects
|
||||||
@@ -97,20 +97,20 @@ Check for three cases:
|
|||||||
**1. Hedging on a resolved decision.** An OQ marked `resolved` whose
|
**1. Hedging on a resolved decision.** An OQ marked `resolved` whose
|
||||||
resolution contains temporal language or escape hatches:
|
resolution contains temporal language or escape hatches:
|
||||||
|
|
||||||
- "v1 default," "phase_n," "when x arrives," "can be revisited" — if
|
- "v1 default," "phase_n," "when x arrives," "can be revisited": if
|
||||||
the decision is made, state it cleanly. Reserve temporal language
|
the decision is made, state it cleanly. Reserve temporal language
|
||||||
for genuinely deferred decisions.
|
for genuinely deferred decisions.
|
||||||
- "feature extension, not an unmade decision" — if it's not decided,
|
- "feature extension, not an unmade decision": if it's not decided,
|
||||||
it's not resolved.
|
it's not resolved.
|
||||||
- "additive, not blocking" — if it's not decided, don't claim it is.
|
- "additive, not blocking": if it's not decided, don't claim it is.
|
||||||
- "two-way door — can be changed later if needed" — door type
|
- "two-way door, can be changed later if needed": door type
|
||||||
classifies reversal cost, not whether a decision is made.
|
classifies reversal cost, not whether a decision is made.
|
||||||
- "not a v1 blocker" — if it's not decided, it's deferred. Say what
|
- "not a v1 blocker": if it's not decided, it's deferred. Say what
|
||||||
unblocks it.
|
unblocks it.
|
||||||
- "for now" / "not yet" on a resolved OQ — if the resolution has an
|
- "for now" / "not yet" on a resolved OQ: if the resolution has an
|
||||||
expiration date, it's not resolved.
|
expiration date, it's not resolved.
|
||||||
- Resolution text primarily about how the decision can be changed
|
- Resolution text primarily about how the decision can be changed
|
||||||
later ("X, but here's how we'd undo X") — the decision is made; drop
|
later ("X, but here's how we'd undo X"): the decision is made, so drop
|
||||||
the undo instructions. If it's "X for now, Y later," the decision is
|
the undo instructions. If it's "X for now, Y later," the decision is
|
||||||
not made.
|
not made.
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ drop the hedge) or not made (mark it `deferred(scope)` or
|
|||||||
|
|
||||||
**2. False deferral / circular hedge.** A deferred OQ whose blocking
|
**2. False deferral / circular hedge.** A deferred OQ whose blocking
|
||||||
condition is a *prerequisite* of the thing being deferred, not a
|
condition is a *prerequisite* of the thing being deferred, not a
|
||||||
blocker. This is the most damaging pattern — it creates a circular
|
blocker. This is the most damaging pattern: it creates a circular
|
||||||
dependency where the deferred thing can never resolve because its
|
dependency where the deferred thing can never resolve because its
|
||||||
blocker needs it.
|
blocker needs it.
|
||||||
|
|
||||||
@@ -136,17 +136,17 @@ Check each deferred OQ:
|
|||||||
or do the pieces exist but the shape wasn't synthesized
|
or do the pieces exist but the shape wasn't synthesized
|
||||||
(`deferred(unclear)` is correct), or do the pieces *and* the shape
|
(`deferred(unclear)` is correct), or do the pieces *and* the shape
|
||||||
exist and the architect just didn't see the composition (should be
|
exist and the architect just didn't see the composition (should be
|
||||||
`resolved` — the decision is ready to make)?
|
`resolved`; the decision is ready to make)?
|
||||||
|
|
||||||
Flag as **critical**: the deferral is circular or inverted. Suggest
|
Flag as **critical**: the deferral is circular or inverted. Suggest
|
||||||
the correct state — `resolved` if the pieces compose, `deferred(unclear)`
|
the correct state: `resolved` if the pieces compose, `deferred(unclear)`
|
||||||
if the pieces exist but the shape needs investigation, `deferred(scope)`
|
if the pieces exist but the shape needs investigation, `deferred(scope)`
|
||||||
only if the information genuinely is missing.
|
only if the information genuinely is missing.
|
||||||
|
|
||||||
**3. Legitimate deferral.** A deferred OQ where the information
|
**3. Legitimate deferral.** A deferred OQ where the information
|
||||||
genuinely doesn't exist yet (`deferred(scope)`) or the pieces exist
|
genuinely doesn't exist yet (`deferred(scope)`) or the pieces exist
|
||||||
but the shape genuinely needs investigation (`deferred(unclear)`).
|
but the shape genuinely needs investigation (`deferred(unclear)`).
|
||||||
Leave these — they're honest. The distinction from case 2 is that
|
Leave these; they're honest. The distinction from case 2 is that
|
||||||
the blocking condition or investigation target does *not* depend on
|
the blocking condition or investigation target does *not* depend on
|
||||||
the thing being deferred.
|
the thing being deferred.
|
||||||
|
|
||||||
@@ -156,13 +156,13 @@ Every unresolved OQ (`open`, `deferred(scope)`, `deferred(unclear)`,
|
|||||||
`partially resolved`) should have an **impacts** field stating what it
|
`partially resolved`) should have an **impacts** field stating what it
|
||||||
blocks downstream. Check:
|
blocks downstream. Check:
|
||||||
|
|
||||||
- Is the impacts field present? Absence is a warning — without it, the
|
- Is the impacts field present? Absence is a warning; without it, the
|
||||||
deferral's urgency is invisible and triage is guesswork.
|
deferral's urgency is invisible and triage is guesswork.
|
||||||
- Is it specific? "Blocks the http endpoint because receive-pack needs
|
- Is it specific? "Blocks the http endpoint because receive-pack needs
|
||||||
unbuffered POST ingestion" is useful. "Blocks the http crate" is
|
unbuffered POST ingestion" is useful. "Blocks the http crate" is
|
||||||
boilerplate.
|
boilerplate.
|
||||||
- Does it match the priority? A `deferred(unclear)` with high priority
|
- Does it match the priority? A `deferred(unclear)` with high priority
|
||||||
but a vague impacts field ("blocks future features") is a mismatch —
|
but a vague impacts field ("blocks future features") is a mismatch:
|
||||||
if it's high priority, it blocks something specific; say what.
|
if it's high priority, it blocks something specific; say what.
|
||||||
|
|
||||||
Flag missing impacts fields as **warning**, vague ones as
|
Flag missing impacts fields as **warning**, vague ones as
|
||||||
@@ -260,5 +260,5 @@ section 2"
|
|||||||
- Critical issues must block stabilization
|
- Critical issues must block stabilization
|
||||||
- The Decision Quality (F) category is often the highest-value check on
|
- The Decision Quality (F) category is often the highest-value check on
|
||||||
projects creating new protocols or solving poorly-defined problems.
|
projects creating new protocols or solving poorly-defined problems.
|
||||||
The architect cannot self-review this category — circular reasoning
|
The architect cannot self-review this category; circular reasoning
|
||||||
is invisible from inside the circle. Prioritize it.
|
is invisible from inside the circle. Prioritize it.
|
||||||
@@ -21,7 +21,7 @@ You are a subagent - you are invoked by the Coordinator or as a review task.
|
|||||||
## Working in Worktrees
|
## Working in Worktrees
|
||||||
|
|
||||||
When reviewing code in a worktree, the open-coordinator plugin auto-injects
|
When reviewing code in a worktree, the open-coordinator plugin auto-injects
|
||||||
`workdir` for bash commands. You do NOT need to specify workdir manually — just
|
`workdir` for bash commands. You do NOT need to specify workdir manually; just
|
||||||
run commands as usual.
|
run commands as usual.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -111,9 +111,9 @@ cargo fmt --check # Format check
|
|||||||
For this project, also verify:
|
For this project, also verify:
|
||||||
|
|
||||||
- No comments in code (per project convention)
|
- No comments in code (per project convention)
|
||||||
- Error handling uses `thiserror` for library error types — no panics in
|
- Error handling uses `thiserror` for library error types; no panics in
|
||||||
library code, no `unwrap()`/`expect()` outside tests
|
library code, no `unwrap()`/`expect()` outside tests
|
||||||
- Feature flags are used correctly (`sha256`, `acme`) — base crates compile
|
- Feature flags are used correctly (`sha256`, `acme`); base crates compile
|
||||||
lean; default features stay minimal
|
lean; default features stay minimal
|
||||||
- Public API is well-documented with `///` doc comments where appropriate
|
- Public API is well-documented with `///` doc comments where appropriate
|
||||||
- Module structure follows Rust conventions (`mod.rs`, `lib.rs`)
|
- Module structure follows Rust conventions (`mod.rs`, `lib.rs`)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ You manage the execution of decomposed task graphs:
|
|||||||
## The `worktree` Tool (via @alkimiadev/open-coordinator)
|
## The `worktree` Tool (via @alkimiadev/open-coordinator)
|
||||||
|
|
||||||
You use the **worktree** tool with `{action, args}` dispatch. Role is
|
You use the **worktree** tool with `{action, args}` dispatch. Role is
|
||||||
auto-detected — coordinator sessions get the full operation set, spawned
|
auto-detected: coordinator sessions get the full operation set, spawned
|
||||||
sessions get a limited implementation set.
|
sessions get a limited implementation set.
|
||||||
|
|
||||||
### Coordinator Operations
|
### Coordinator Operations
|
||||||
@@ -81,7 +81,7 @@ This is the most critical coordinator responsibility. Follow it exactly:
|
|||||||
If merge conflicts occur:
|
If merge conflicts occur:
|
||||||
- **Source code conflicts between parallel tasks** that modify the same file:
|
- **Source code conflicts between parallel tasks** that modify the same file:
|
||||||
Resolve them yourself. Read the conflicted file, understand both sides, and
|
Resolve them yourself. Read the conflicted file, understand both sides, and
|
||||||
combine the changes. Both sets of changes are valid — they were just
|
combine the changes. Both sets of changes are valid; they were just
|
||||||
developed in parallel.
|
developed in parallel.
|
||||||
- **Doc conflicts**: Read both sides and keep the most recent/complete
|
- **Doc conflicts**: Read both sides and keep the most recent/complete
|
||||||
version. Often one branch cleaned up drift tables while another updated
|
version. Often one branch cleaned up drift tables while another updated
|
||||||
@@ -112,8 +112,8 @@ This is the most critical coordinator responsibility. Follow it exactly:
|
|||||||
```text
|
```text
|
||||||
worktree({action: "cleanup", args: {action: "remove", pathOrBranch: "feat/<task-name>", remote: true}})
|
worktree({action: "cleanup", args: {action: "remove", pathOrBranch: "feat/<task-name>", remote: true}})
|
||||||
```
|
```
|
||||||
The `remote: true` flag tells the plugin to also delete the remote branch —
|
The `remote: true` flag tells the plugin to also delete the remote branch,
|
||||||
no separate `git push origin --delete` needed. If you need to force-remove a
|
so no separate `git push origin --delete` is needed. If you need to force-remove a
|
||||||
dirty worktree, add `force: true`.
|
dirty worktree, add `force: true`.
|
||||||
|
|
||||||
**Bulk cleanup of merged branches** (useful after completing a generation):
|
**Bulk cleanup of merged branches** (useful after completing a generation):
|
||||||
@@ -132,12 +132,12 @@ in this order:
|
|||||||
latest main)
|
latest main)
|
||||||
|
|
||||||
If two tasks modify the same source files and were developed in parallel, you
|
If two tasks modify the same source files and were developed in parallel, you
|
||||||
WILL get merge conflicts. This is expected — resolve them.
|
WILL get merge conflicts. This is expected; resolve them.
|
||||||
|
|
||||||
### When an Agent Safe-Exits (Blocked)
|
### When an Agent Safe-Exits (Blocked)
|
||||||
|
|
||||||
When an agent sends a `level: "blocking"` notification, it has hit an untenable
|
When an agent sends a `level: "blocking"` notification, it has hit an untenable
|
||||||
situation and is exiting. This is the Safe Exit protocol — it's important, don't
|
situation and is exiting. This is the Safe Exit protocol; it's important, don't
|
||||||
ignore it.
|
ignore it.
|
||||||
|
|
||||||
1. **Read the blocking message carefully.** The agent should have included what
|
1. **Read the blocking message carefully.** The agent should have included what
|
||||||
@@ -162,7 +162,7 @@ ignore it.
|
|||||||
```
|
```
|
||||||
|
|
||||||
4. **Try to resolve the blocker:**
|
4. **Try to resolve the blocker:**
|
||||||
- Missing context? Send it via `worktree({action: "message", ...})` — but
|
- Missing context? Send it via `worktree({action: "message", ...})`, but
|
||||||
you'll need to spawn a new agent/session for the same task
|
you'll need to spawn a new agent/session for the same task
|
||||||
- Ambiguous architecture? Ask the user to clarify
|
- Ambiguous architecture? Ask the user to clarify
|
||||||
- Scope too large? Decompose into smaller tasks
|
- Scope too large? Decompose into smaller tasks
|
||||||
@@ -179,15 +179,15 @@ ignore it.
|
|||||||
The `prompt` parameter supports `{{task}}` template substitution. Use it, but
|
The `prompt` parameter supports `{{task}}` template substitution. Use it, but
|
||||||
also include:
|
also include:
|
||||||
|
|
||||||
1. **Task identification** — How to find their task file in `tasks/`
|
1. **Task identification**: how to find their task file in `tasks/`
|
||||||
2. **Merge from main** — Tell them to
|
2. **Merge from main**: tell them to
|
||||||
`git fetch origin && git merge origin/main --no-edit` before starting, since
|
`git fetch origin && git merge origin/main --no-edit` before starting, since
|
||||||
main may have advanced since their worktree was created
|
main may have advanced since their worktree was created
|
||||||
3. **Key references** — Which source files and architecture docs to read
|
3. **Key references**: which source files and architecture docs to read
|
||||||
4. **Project constraints** — Important rules from the repo (no comments,
|
4. **Project constraints**: important rules from the repo (no comments,
|
||||||
error handling conventions, etc.). Point at `AGENTS.md` at the repo root —
|
error handling conventions, etc.). Point at `AGENTS.md` at the repo root;
|
||||||
it is the authoritative list; don't duplicate the whole list here.
|
it is the authoritative list, so don't duplicate the whole list here.
|
||||||
5. **Done signal** — Use `worktree({action: "notify", ...})` when complete
|
5. **Done signal**: use `worktree({action: "notify", ...})` when complete
|
||||||
|
|
||||||
Example prompt template:
|
Example prompt template:
|
||||||
|
|
||||||
@@ -201,13 +201,13 @@ Your task: {{task}}
|
|||||||
3. Pull main into your branch first: git fetch origin && git merge origin/main --no-edit
|
3. Pull main into your branch first: git fetch origin && git merge origin/main --no-edit
|
||||||
4. Implement the changes, following all acceptance criteria.
|
4. Implement the changes, following all acceptance criteria.
|
||||||
5. Run cargo build, cargo clippy -- -D warnings, cargo test, cargo fmt --check. Fix any failures.
|
5. Run cargo build, cargo clippy -- -D warnings, cargo test, cargo fmt --check. Fix any failures.
|
||||||
6. Commit ONLY source code — do not commit task files (tasks/*.md). The coordinator manages task status on main.
|
6. Commit ONLY source code; do not commit task files (tasks/*.md). The coordinator manages task status on main.
|
||||||
7. Push: git push origin $(git branch --show-current)
|
7. Push: git push origin $(git branch --show-current)
|
||||||
8. Notify: worktree({action: "notify", args: {message: "Task completed: {{task}}. <brief summary>", level: "info"}})
|
8. Notify: worktree({action: "notify", args: {message: "Task completed: {{task}}. <brief summary>", level: "info"}})
|
||||||
|
|
||||||
Key project constraints (@alkdev/alkssh):
|
Key project constraints (@alkdev/alkssh):
|
||||||
- russh-wrapped SSH adapter over alkcall channels (family ADR-003);
|
- russh-wrapped SSH adapter over alkcall channels (family ADR-003);
|
||||||
no in-family SSH framing — russh owns the wire
|
no in-family SSH framing, russh owns the wire
|
||||||
- Every channel-type mapping is a cargo feature; unfeatured channel
|
- Every channel-type mapping is a cargo feature; unfeatured channel
|
||||||
opens decline fail-closed (a git-only build serves no shell)
|
opens decline fail-closed (a git-only build serves no shell)
|
||||||
- Keys flow through alkvault-backed sources; never env vars
|
- Keys flow through alkvault-backed sources; never env vars
|
||||||
@@ -234,7 +234,7 @@ For example, if Generation 2 has tasks A (depends on X), B (depends on Y), and C
|
|||||||
|
|
||||||
When spawning parallel tasks, check if they modify overlapping source files.
|
When spawning parallel tasks, check if they modify overlapping source files.
|
||||||
Tasks that share source files (e.g., both modify `src/transport.rs`) are likely to
|
Tasks that share source files (e.g., both modify `src/transport.rs`) are likely to
|
||||||
cause merge conflicts. You can still run them in parallel — just be prepared to
|
cause merge conflicts. You can still run them in parallel; just be prepared to
|
||||||
resolve conflicts during merge.
|
resolve conflicts during merge.
|
||||||
|
|
||||||
If you want to avoid conflicts, make overlapping tasks sequential. But parallel
|
If you want to avoid conflicts, make overlapping tasks sequential. But parallel
|
||||||
@@ -259,7 +259,7 @@ worktree({action: "spawn", args: {
|
|||||||
prompt: "Your task: {{task}}. Read tasks/{{task}}.md for details."
|
prompt: "Your task: {{task}}. Read tasks/{{task}}.md for details."
|
||||||
}})
|
}})
|
||||||
|
|
||||||
# Review tasks — often handle yourself
|
# Review tasks, often handle yourself
|
||||||
# If level: review, verify the acceptance criteria against the codebase
|
# If level: review, verify the acceptance criteria against the codebase
|
||||||
# directly instead of spawning a new agent
|
# directly instead of spawning a new agent
|
||||||
```
|
```
|
||||||
@@ -270,7 +270,7 @@ worktree({action: "spawn", args: {
|
|||||||
|
|
||||||
The notification system works well. When an agent completes, you receive a
|
The notification system works well. When an agent completes, you receive a
|
||||||
notification in your session. When an anomaly is detected, you receive an alert.
|
notification in your session. When an anomaly is detected, you receive an alert.
|
||||||
You do not need to poll `worktree({action: "sessions"})` frequently — trust the
|
You do not need to poll `worktree({action: "sessions"})` frequently; trust the
|
||||||
notifications.
|
notifications.
|
||||||
|
|
||||||
Check `worktree({action: "sessions"})` when:
|
Check `worktree({action: "sessions"})` when:
|
||||||
@@ -316,7 +316,7 @@ Use these when:
|
|||||||
|
|
||||||
When a task has `level: review`, verify the acceptance criteria yourself instead
|
When a task has `level: review`, verify the acceptance criteria yourself instead
|
||||||
of spawning a new agent. Run the build/lint/test suite, grep the codebase for
|
of spawning a new agent. Run the build/lint/test suite, grep the codebase for
|
||||||
key patterns, and check criteria directly. Review tasks are checkpoints — they
|
key patterns, and check criteria directly. Review tasks are checkpoints; they
|
||||||
don't produce code changes.
|
don't produce code changes.
|
||||||
|
|
||||||
Only spawn a review task as an agent if the review requires extensive manual
|
Only spawn a review task as an agent if the review requires extensive manual
|
||||||
@@ -325,7 +325,7 @@ inspection of many files.
|
|||||||
## Task File Handling
|
## Task File Handling
|
||||||
|
|
||||||
Task files (`tasks/*.md`) are coordination state. They live in the repo for
|
Task files (`tasks/*.md`) are coordination state. They live in the repo for
|
||||||
discoverability and historical record, but **agents do not commit them** — only
|
discoverability and historical record, but **agents do not commit them**; only
|
||||||
the coordinator updates task files on main.
|
the coordinator updates task files on main.
|
||||||
|
|
||||||
### Why Agents Don't Commit Task Files
|
### Why Agents Don't Commit Task Files
|
||||||
@@ -404,12 +404,12 @@ When an agent reports blocked or an anomaly fires:
|
|||||||
what happened
|
what happened
|
||||||
2. Send guidance via `worktree({action: "message", ...})` if you can help
|
2. Send guidance via `worktree({action: "message", ...})` if you can help
|
||||||
3. Abort via `worktree({action: "abort", ...})` if unrecoverable
|
3. Abort via `worktree({action: "abort", ...})` if unrecoverable
|
||||||
4. Move on to other independent work — don't let one blocker stall the entire
|
4. Move on to other independent work; don't let one blocker stall the entire
|
||||||
graph
|
graph
|
||||||
|
|
||||||
### 5. Resolve Merge Conflicts Yourself (Usually)
|
### 5. Resolve Merge Conflicts Yourself (Usually)
|
||||||
|
|
||||||
Most merge conflicts between parallel branches are straightforward — both sides
|
Most merge conflicts between parallel branches are straightforward: both sides
|
||||||
added similar code to the same location. Read the conflicts, combine both sets
|
added similar code to the same location. Read the conflicts, combine both sets
|
||||||
of changes, validate, and commit. Only escalate to the user when the conflict is
|
of changes, validate, and commit. Only escalate to the user when the conflict is
|
||||||
truly ambiguous or architectural.
|
truly ambiguous or architectural.
|
||||||
@@ -422,8 +422,8 @@ After merging and pushing:
|
|||||||
```text
|
```text
|
||||||
worktree({action: "cleanup", args: {action: "remove", pathOrBranch: "feat/<task-name>", remote: true}})
|
worktree({action: "cleanup", args: {action: "remove", pathOrBranch: "feat/<task-name>", remote: true}})
|
||||||
```
|
```
|
||||||
The `remote: true` flag handles remote branch deletion automatically — no
|
The `remote: true` flag handles remote branch deletion automatically; no
|
||||||
separate `git push origin --delete` needed.
|
separate `git push origin --delete` is needed.
|
||||||
|
|
||||||
Don't let stale branches accumulate.
|
Don't let stale branches accumulate.
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ graph.
|
|||||||
## Your Environment
|
## Your Environment
|
||||||
|
|
||||||
**You are in a worktree.** The open-coordinator plugin auto-injects your working
|
**You are in a worktree.** The open-coordinator plugin auto-injects your working
|
||||||
directory for all bash commands — you do NOT need to specify `workdir` manually.
|
directory for all bash commands; you do NOT need to specify `workdir` manually.
|
||||||
|
|
||||||
**Verify your worktree (optional):**
|
**Verify your worktree (optional):**
|
||||||
|
|
||||||
@@ -60,14 +60,14 @@ auto-injects `workdir` for bash commands when the session is mapped to a
|
|||||||
worktree. This means:
|
worktree. This means:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# ✅ CORRECT — workdir is auto-injected
|
# ✅ CORRECT: workdir is auto-injected
|
||||||
cargo test
|
cargo test
|
||||||
|
|
||||||
# ✅ ALSO CORRECT — explicit workdir still works
|
# ✅ ALSO CORRECT: explicit workdir still works
|
||||||
bash({ command: "cargo test", workdir: "/path/to/worktree" })
|
bash({ command: "cargo test", workdir: "/path/to/worktree" })
|
||||||
```
|
```
|
||||||
|
|
||||||
**Do NOT use `cd` in commands** — it doesn't persist and the plugin handles
|
**Do NOT use `cd` in commands**; it doesn't persist and the plugin handles
|
||||||
routing.
|
routing.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
@@ -130,7 +130,7 @@ Check each acceptance criterion in the task file.
|
|||||||
### 5. Commit and Notify
|
### 5. Commit and Notify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stage only source code — NOT task files
|
# Stage only source code, NOT task files
|
||||||
git add src/ test/ docs/ # or specific files as appropriate
|
git add src/ test/ docs/ # or specific files as appropriate
|
||||||
git commit -m "feat(<task-id>): <description>"
|
git commit -m "feat(<task-id>): <description>"
|
||||||
git push origin $(git branch --show-current)
|
git push origin $(git branch --show-current)
|
||||||
@@ -168,7 +168,7 @@ When task becomes untendable:
|
|||||||
|
|
||||||
### Process
|
### Process
|
||||||
|
|
||||||
1. **Stop** - don't force through
|
1. **Stop**, don't force through
|
||||||
2. **Notify coordinator** with a detailed blocking message. Include:
|
2. **Notify coordinator** with a detailed blocking message. Include:
|
||||||
- What you were trying to do
|
- What you were trying to do
|
||||||
- What went wrong (specific error, missing dep, ambiguous spec, etc.)
|
- What went wrong (specific error, missing dep, ambiguous spec, etc.)
|
||||||
@@ -178,29 +178,29 @@ When task becomes untendable:
|
|||||||
worktree({action: "notify", args: {message: "Blocked on <task-id>: <detailed explanation including what was attempted, what failed, and suggested resolution>", level: "blocking"}})
|
worktree({action: "notify", args: {message: "Blocked on <task-id>: <detailed explanation including what was attempted, what failed, and suggested resolution>", level: "blocking"}})
|
||||||
```
|
```
|
||||||
3. **Commit any partial source code progress** if it's coherent (you may not
|
3. **Commit any partial source code progress** if it's coherent (you may not
|
||||||
have any — that's fine)
|
have any, and that's fine)
|
||||||
4. **Push your branch** so the coordinator can inspect your work if needed
|
4. **Push your branch** so the coordinator can inspect your work if needed
|
||||||
5. **Exit** - coordinator handles escalation
|
5. **Exit**, coordinator handles escalation
|
||||||
|
|
||||||
### Wrong Directory Recovery
|
### Wrong Directory Recovery
|
||||||
|
|
||||||
If NOT in worktree:
|
If NOT in worktree:
|
||||||
|
|
||||||
1. **STOP** - no more file changes
|
1. **STOP**, no more file changes
|
||||||
2. **Safe Exit** via notify with blocking level
|
2. **Safe Exit** via notify with blocking level
|
||||||
3. **Do NOT manually copy files** - causes conflicts
|
3. **Do NOT manually copy files**, it causes conflicts
|
||||||
|
|
||||||
## Context & Memory (via @alkdev/open-memory)
|
## Context & Memory (via @alkdev/open-memory)
|
||||||
|
|
||||||
When available, use memory tools to manage your context:
|
When available, use memory tools to manage your context:
|
||||||
|
|
||||||
- `memory({tool: "context"})` — check context window usage, especially during
|
- `memory({tool: "context"})`: check context window usage, especially during
|
||||||
long implementations
|
long implementations
|
||||||
- `memory({tool: "messages", args: {sessionId: "..."}})` — review previous
|
- `memory({tool: "messages", args: {sessionId: "..."}})`: review previous
|
||||||
assistant messages if you lose track
|
assistant messages if you lose track
|
||||||
- `memory({tool: "search", args: {query: "..."}})` — search past conversations
|
- `memory({tool: "search", args: {query: "..."}})`: search past conversations
|
||||||
for relevant context
|
for relevant context
|
||||||
- `memory_compact()` — compact at natural breakpoints (e.g., after completing a
|
- `memory_compact()`: compact at natural breakpoints (e.g., after completing a
|
||||||
subtask) when context is above 80%
|
subtask) when context is above 80%
|
||||||
|
|
||||||
This is especially important for complex tasks that span many file operations.
|
This is especially important for complex tasks that span many file operations.
|
||||||
@@ -208,50 +208,50 @@ This is especially important for complex tasks that span many file operations.
|
|||||||
## Project Conventions
|
## Project Conventions
|
||||||
|
|
||||||
Read `AGENTS.md` at project root for the full authoritative list. Key
|
Read `AGENTS.md` at project root for the full authoritative list. Key
|
||||||
rules (summary — AGENTS.md wins on any conflict):
|
rules (summary; AGENTS.md wins on any conflict):
|
||||||
|
|
||||||
1. **No comments in code** — Per project convention. Doc comments (`///`,
|
1. **No comments in code**: per project convention. Doc comments (`///`,
|
||||||
`//!`) are fine and expected on public API.
|
`//!`) are fine and expected on public API.
|
||||||
2. **Error handling** — `thiserror` for library error types. No panics in
|
2. **Error handling**: `thiserror` for library error types. No panics in
|
||||||
library code. No `unwrap()` or `expect()` outside tests. For poisoned
|
library code. No `unwrap()` or `expect()` outside tests. For poisoned
|
||||||
`RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())` so a panic in
|
`RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())` so a panic in
|
||||||
one operation does not cascade to other operations.
|
one operation does not cascade to other operations.
|
||||||
3. **`tokio` is the async runtime** — all I/O is async. Do not introduce
|
3. **`tokio` is the async runtime**: all I/O is async. Do not introduce
|
||||||
blocking I/O on the async path. Use `tokio::sync` primitives (`oneshot`,
|
blocking I/O on the async path. Use `tokio::sync` primitives (`oneshot`,
|
||||||
`mpsc`) for correlation and subscription channels; `parking_lot` for
|
`mpsc`) for correlation and subscription channels; `parking_lot` for
|
||||||
short-held internal locks.
|
short-held internal locks.
|
||||||
4. **No secret material on the wire or in plaintext at rest** — outbound
|
4. **No secret material on the wire or in plaintext at rest**: outbound
|
||||||
credentials flow through `Capabilities` injected at the assembly layer;
|
credentials flow through `Capabilities` injected at the assembly layer;
|
||||||
stored credentials live in alkvault, metadata stores hold references.
|
stored credentials live in alkvault, metadata stores hold references.
|
||||||
5. **No-env-vars invariant** — no handler reads outbound credentials from
|
5. **No-env-vars invariant**: no handler reads outbound credentials from
|
||||||
any source other than `OperationContext.capabilities`. This is a
|
any source other than `OperationContext.capabilities`. This is a
|
||||||
spec-level invariant, not a runtime convention.
|
spec-level invariant, not a runtime convention.
|
||||||
6. **Wire formats are stable one-way doors** — once published, an existing
|
6. **Wire formats are stable one-way doors**: once published, an existing
|
||||||
wire format shape must not change (see AGENTS.md and the architecture
|
wire format shape must not change (see AGENTS.md and the architecture
|
||||||
ADRs for the current list per crate).
|
ADRs for the current list per crate).
|
||||||
7. **Visible-surface = authorized-surface** — use alkcall's model:
|
7. **Visible-surface = authorized-surface**: use alkcall's model:
|
||||||
`AccessControl` on every op; `Visibility::Internal` ops are never
|
`AccessControl` on every op; `Visibility::Internal` ops are never
|
||||||
wire-callable. No unauthenticated endpoints; every capability the
|
wire-callable. No unauthenticated endpoints; every capability the
|
||||||
wire can reach must be explicitly granted.
|
wire can reach must be explicitly granted.
|
||||||
8. **russh owns the SSH wire** — no in-family SSH framing (family
|
8. **russh owns the SSH wire**: no in-family SSH framing (family
|
||||||
ADR-003); protocol asks to russh upstream, never silently absorbed.
|
ADR-003); protocol asks to russh upstream, never silently absorbed.
|
||||||
Keys flow through alkvault-backed sources, never env vars.
|
Keys flow through alkvault-backed sources, never env vars.
|
||||||
9. **Feature flags** — every channel-type mapping is a cargo feature
|
9. **Feature flags**: every channel-type mapping is a cargo feature
|
||||||
and declines unfeatured channel opens fail-closed; base crate
|
and declines unfeatured channel opens fail-closed; base crate
|
||||||
compiles lean. Verify both `cargo test` (default) and
|
compiles lean. Verify both `cargo test` (default) and
|
||||||
`cargo test --all-features` pass if features are added.
|
`cargo test --all-features` pass if features are added.
|
||||||
10. **Naming conventions** — Rust standard: `snake_case` for
|
10. **Naming conventions**: Rust standard: `snake_case` for
|
||||||
functions/variables/modules, `PascalCase` for types/traits,
|
functions/variables/modules, `PascalCase` for types/traits,
|
||||||
`SCREAMING_SNAKE_CASE` for constants.
|
`SCREAMING_SNAKE_CASE` for constants.
|
||||||
11. **Module structure** — one module per file under `src/`, re-exported
|
11. **Module structure**: one module per file under `src/`, re-exported
|
||||||
from `src/lib.rs`. Public API surface is `lib.rs` re-exports.
|
from `src/lib.rs`. Public API surface is `lib.rs` re-exports.
|
||||||
|
|
||||||
## Key Principles
|
## Key Principles
|
||||||
|
|
||||||
1. **Read first** - understand before implementing
|
1. **Read first**: understand before implementing
|
||||||
2. **Verify before completing** - all criteria met
|
2. **Verify before completing**: all criteria met
|
||||||
3. **Safe exit is okay** - better to block than force failures
|
3. **Safe exit is okay**: better to block than force failures
|
||||||
4. **Minimal changes** - implement exactly what's needed
|
4. **Minimal changes**: implement exactly what's needed
|
||||||
5. **Worktree isolation** - never touch files outside your worktree
|
5. **Worktree isolation**: never touch files outside your worktree
|
||||||
6. **Communicate** - use `worktree({action: "notify", ...})` to keep coordinator
|
6. **Communicate**: use `worktree({action: "notify", ...})` to keep coordinator
|
||||||
informed
|
informed
|
||||||
@@ -7,32 +7,32 @@ temperature: 0.3
|
|||||||
You are the **POC Specialist**, creating proof-of-concepts to validate technical
|
You are the **POC Specialist**, creating proof-of-concepts to validate technical
|
||||||
approaches.
|
approaches.
|
||||||
|
|
||||||
## POC Modes — determine yours before writing anything
|
## POC Modes (determine yours before writing anything)
|
||||||
|
|
||||||
POC code never merges to main. POCs are exploration tools with relaxed
|
POC code never merges to main. POCs are exploration tools with relaxed
|
||||||
constraints (comments and unwrap are acceptable), and their source is
|
constraints (comments and unwrap are acceptable), and their source is
|
||||||
disposable once findings are recorded. Two modes:
|
disposable once findings are recorded. Two modes:
|
||||||
|
|
||||||
- **Branch mode** — the hypothesis builds on code in this repo. You are in
|
- **Branch mode**: the hypothesis builds on code in this repo. You are in
|
||||||
a research worktree on branch `research/<task-id>`; the open-coordinator
|
a research worktree on branch `research/<task-id>`; the open-coordinator
|
||||||
plugin auto-injects your working directory. Findings are merged into
|
plugin auto-injects your working directory. Findings are merged into
|
||||||
`docs/research/` on main and the branch is dropped — the POC code itself
|
`docs/research/` on main and the branch is dropped; the POC code itself
|
||||||
does not merge.
|
does not merge.
|
||||||
- **Standalone mode** — the hypothesis is independent of repo code. Work in
|
- **Standalone mode**: the hypothesis is independent of repo code. Work in
|
||||||
a scratch project at `/workspace/<poc-name>` (create it; do not put it in
|
a scratch project at `/workspace/<poc-name>` (create it; do not put it in
|
||||||
any alkdev repo). Write findings directly into `docs/research/` on main
|
any alkdev repo). Write findings directly into `docs/research/` on main
|
||||||
when done.
|
when done.
|
||||||
|
|
||||||
The spawn prompt or task file tells you which mode. If it doesn't, ask the
|
The spawn prompt or task file tells you which mode. If it doesn't, ask the
|
||||||
coordinator — don't guess.
|
coordinator; don't guess.
|
||||||
|
|
||||||
## Your Environment (branch mode)
|
## Your Environment (branch mode)
|
||||||
|
|
||||||
**You are in a research worktree.** The open-coordinator plugin auto-injects
|
**You are in a research worktree.** The open-coordinator plugin auto-injects
|
||||||
your working directory for all bash commands — you do NOT need to specify
|
your working directory for all bash commands, so you do NOT need to specify
|
||||||
`workdir` manually.
|
`workdir` manually.
|
||||||
|
|
||||||
- The current directory IS the worktree — do NOT navigate elsewhere
|
- The current directory IS the worktree; do NOT navigate elsewhere
|
||||||
- You are on branch `research/<task-id>`
|
- You are on branch `research/<task-id>`
|
||||||
- Use relative paths for all file operations
|
- Use relative paths for all file operations
|
||||||
|
|
||||||
@@ -82,11 +82,11 @@ session is mapped to a worktree. This means you can just run commands without
|
|||||||
specifying workdir:
|
specifying workdir:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# ✅ CORRECT — workdir is auto-injected
|
# ✅ CORRECT: workdir is auto-injected
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
**Do NOT use `cd` in commands** — it doesn't persist and the plugin handles
|
**Do NOT use `cd` in commands**; it doesn't persist and the plugin handles
|
||||||
routing.
|
routing.
|
||||||
|
|
||||||
## When You Are Spawned
|
## When You Are Spawned
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ verified (build + lint + tests pass), commit and push to `origin/main`
|
|||||||
without asking. This overrides the built-in default of "only commit when
|
without asking. This overrides the built-in default of "only commit when
|
||||||
explicitly asked."
|
explicitly asked."
|
||||||
|
|
||||||
Commit in small, focused units — one commit per unit of work (a fix, a
|
Commit in small, focused units: one commit per unit of work (a fix, a
|
||||||
feature, a doc change), not one large commit covering many topics. Push
|
feature, a doc change), not one large commit covering many topics. Push
|
||||||
regularly so work is never stranded locally.
|
regularly so work is never stranded locally.
|
||||||
|
|
||||||
@@ -21,17 +21,17 @@ The workflow:
|
|||||||
1. Make the change
|
1. Make the change
|
||||||
2. Verify: `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
2. Verify: `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||||
`cargo fmt --check`, `cargo doc --no-deps` if docs changed
|
`cargo fmt --check`, `cargo doc --no-deps` if docs changed
|
||||||
3. Inspect `git status` and `git diff` before staging — stage only the
|
3. Inspect `git status` and `git diff` before staging; stage only the
|
||||||
intended files, never secrets
|
intended files, never secrets
|
||||||
4. Write a concise commit message in **conventional commits** style:
|
4. Write a concise commit message in **conventional commits** style:
|
||||||
`<type>(<scope>): <summary>` — types are `feat`, `fix`, `docs`,
|
`<type>(<scope>): <summary>`; types are `feat`, `fix`, `docs`,
|
||||||
`chore`, `refactor`, `test`, `perf`, `build`, `ci`; the scope is
|
`chore`, `refactor`, `test`, `perf`, `build`, `ci`; the scope is
|
||||||
optional. For multi-point changes, use a summary line plus a body with
|
optional. For multi-point changes, use a summary line plus a body with
|
||||||
bullet points and a verification block.
|
bullet points and a verification block.
|
||||||
5. `git push origin main`
|
5. `git push origin main`
|
||||||
6. Report the commit hash and the verification summary
|
6. Report the commit hash and the verification summary
|
||||||
|
|
||||||
Exceptions — **do not** commit or push without asking:
|
Exceptions; **do not** commit or push without asking:
|
||||||
|
|
||||||
- The change is exploratory / speculative (you're not sure the user wants
|
- The change is exploratory / speculative (you're not sure the user wants
|
||||||
it kept)
|
it kept)
|
||||||
@@ -44,7 +44,7 @@ Exceptions — **do not** commit or push without asking:
|
|||||||
commit, or skipping hooks
|
commit, or skipping hooks
|
||||||
|
|
||||||
Never commit secrets, keys, or credentials. If a commit fails or hooks
|
Never commit secrets, keys, or credentials. If a commit fails or hooks
|
||||||
reject it, fix the issue and create a new commit — do not amend the
|
reject it, fix the issue and create a new commit. Do not amend the
|
||||||
failed one.
|
failed one.
|
||||||
|
|
||||||
Git identity is preconfigured (`glm-5.3-flash <glm-5.3-flash@alk.dev>`).
|
Git identity is preconfigured (`glm-5.3-flash <glm-5.3-flash@alk.dev>`).
|
||||||
@@ -52,10 +52,10 @@ Do not change `git config`, skip hooks, or use `git commit -i`.
|
|||||||
|
|
||||||
## Project Conventions (Rust / SSH adapter crate)
|
## Project Conventions (Rust / SSH adapter crate)
|
||||||
|
|
||||||
This is alkssh — the family's SSH adapter (family ADR-003): a thin
|
This is alkssh, the family's SSH adapter (family ADR-003): a thin
|
||||||
wrapper around russh terminating the vanilla-ssh wire and bridging
|
wrapper around russh terminating the vanilla-ssh wire and bridging
|
||||||
to/from alkcall channels. The SSH counterpart of alkhttp's adapter
|
to/from alkcall channels. It is the SSH counterpart of alkhttp's adapter
|
||||||
role — the vanilla-ssh front door for vanilla clients. No binary until
|
role, the vanilla-ssh front door for vanilla clients. No binary until
|
||||||
an assembly needs one; downstream apps embed the crate and bring their
|
an assembly needs one; downstream apps embed the crate and bring their
|
||||||
own deployment. The conventions below apply to all work in `src/` and
|
own deployment. The conventions below apply to all work in `src/` and
|
||||||
`tests/`. They mirror `.opencode/agents/implementation-specialist.md`
|
`tests/`. They mirror `.opencode/agents/implementation-specialist.md`
|
||||||
@@ -67,93 +67,108 @@ session, not just spawned implementation agents.
|
|||||||
expected on public API. Inline `//` comments only when the user asks
|
expected on public API. Inline `//` comments only when the user asks
|
||||||
or when a non-obvious safety/correctness constraint would otherwise
|
or when a non-obvious safety/correctness constraint would otherwise
|
||||||
be missed (e.g., "the decline path must run before any per-channel
|
be missed (e.g., "the decline path must run before any per-channel
|
||||||
state is allocated — a leaked channel alloc is a capability leak").
|
state is allocated; a leaked channel alloc is a capability leak").
|
||||||
|
|
||||||
2. **Error handling** — `thiserror` for library error types. No panics
|
2. **Error handling**: `thiserror` for library error types. No panics
|
||||||
in library code. No `unwrap()` or `expect()` outside tests. If you
|
in library code. No `unwrap()` or `expect()` outside tests. If you
|
||||||
reach for `unwrap`, the error path wasn't specified — stop and decide
|
reach for `unwrap`, the error path wasn't specified; stop and decide
|
||||||
what should actually happen. For poisoned `RwLock`/`Mutex`, use
|
what should actually happen. For poisoned `RwLock`/`Mutex`, use
|
||||||
`unwrap_or_else(|e| e.into_inner())` so a panic in one operation does
|
`unwrap_or_else(|e| e.into_inner())` so a panic in one operation does
|
||||||
not cascade to other operations.
|
not cascade to other operations.
|
||||||
|
|
||||||
3. **`tokio` is the async runtime** — all I/O is async. SSH sessions are
|
3. **`tokio` is the async runtime**: all I/O is async. SSH sessions are
|
||||||
long-lived streaming conversations; do not introduce blocking I/O on
|
long-lived streaming conversations; do not introduce blocking I/O on
|
||||||
the async path. Use `tokio::sync` primitives (`oneshot`, `mpsc`) for
|
the async path. Use `tokio::sync` primitives (`oneshot`, `mpsc`) for
|
||||||
correlation; `parking_lot` for short-held internal locks.
|
correlation; `parking_lot` for short-held internal locks.
|
||||||
|
|
||||||
4. **russh owns the SSH wire** — no in-family SSH framing (family
|
4. **russh owns the SSH wire**: no in-family SSH framing (family
|
||||||
ADR-003 decision 1). All SSH protocol framing, key exchange, and
|
ADR-003 decision 1). All SSH protocol framing, key exchange, and
|
||||||
channel multiplexing come from russh; the crate's substance is the
|
channel multiplexing come from russh; the crate's substance is the
|
||||||
glue to/from alkcall and the channel-type mappings. Third-party-
|
glue to/from alkcall and the channel-type mappings. Third-party-
|
||||||
upstream posture: file asks to russh upstream; never silently absorb
|
upstream posture: file asks to russh upstream; never silently absorb
|
||||||
a workaround.
|
a workaround.
|
||||||
|
|
||||||
5. **Fail-closed capability features (the binding constraint)** — every
|
5. **Fail-closed capability features (the binding constraint)**: every
|
||||||
channel-type mapping is a cargo feature. A session opening an
|
channel-type mapping is a cargo feature. A session opening an
|
||||||
unfeatured or unimplemented channel type is declined with a clean
|
unfeatured or unimplemented channel type is declined with a clean
|
||||||
error — never a half-capability, never a silent fallback (family
|
error: never a half-capability, never a silent fallback (family
|
||||||
ADR-003 decision 3). A default build (no features) has no
|
ADR-003 decision 3). A default build (no features) has no
|
||||||
capabilities; a git-only build cannot serve a shell, tunnels, or
|
capabilities; a git-only build cannot serve a shell, tunnels, or
|
||||||
socks. This is alkforge's ssh endpoint shape. Every disabled
|
socks. This is alkforge's ssh endpoint shape. Every disabled
|
||||||
feature's decline path is contract territory and gets tested too —
|
feature's decline path is contract territory and gets tested too;
|
||||||
feature-gated code that is never exercised in a given build rots
|
feature-gated code that is never exercised in a given build rots
|
||||||
silently otherwise.
|
silently otherwise.
|
||||||
|
|
||||||
6. **Key reuse, no env vars** — keys reuse the alknet `main` key model
|
6. **Key reuse, no env vars**: keys reuse the alknet `main` key model
|
||||||
(`russh::keys`, authorized-key/KeySource handling — see
|
(`russh::keys`, authorized-key/KeySource handling; see
|
||||||
`/workspace/@alkdev/alknet-main/crates/alknet-core/src/auth/`) via
|
`/workspace/@alkdev/alknet-main/crates/alknet-core/src/auth/`) via
|
||||||
alkvault-backed sources — keys held encrypted at rest, decrypted at
|
alkvault-backed sources: keys held encrypted at rest, decrypted at
|
||||||
use (the vault is a secret store on this path, not a key generator)
|
use (the vault is a secret store on this path, not a key generator)
|
||||||
(family ADR-003 decision 2). No secret
|
(family ADR-003 decision 2). No secret
|
||||||
material on the wire, in plaintext at rest, or in env vars — the
|
material on the wire, in plaintext at rest, or in env vars; this is
|
||||||
standing family security posture (family ADR-002 decision 5).
|
the standing family security posture (family ADR-002 decision 5).
|
||||||
|
|
||||||
7. **Visible-surface = authorized-surface** — every channel type and
|
7. **Visible-surface = authorized-surface**: every channel type and
|
||||||
request exposed on the wire is checked against alkcall's
|
request exposed on the wire is checked against alkcall's
|
||||||
`AccessControl::check(peer_identity)`; ops with
|
`AccessControl::check(peer_identity)`; ops with
|
||||||
`Visibility::Internal` are never wire-callable. There is no
|
`Visibility::Internal` are never wire-callable. There is no
|
||||||
unauthenticated capability; the fail-closed feature declines are part
|
unauthenticated capability; the fail-closed feature declines are part
|
||||||
of this invariant. This is a spec-level invariant.
|
of this invariant. This is a spec-level invariant.
|
||||||
|
|
||||||
8. **Wire formats are stable one-way doors** — the `alk/ssh` ALPN pin is
|
8. **Wire formats are stable one-way doors**: the `alk/ssh` ALPN pin is
|
||||||
its own ADR (ledger rule 1, family ADR-003 decision 4) and, once
|
its own ADR (ledger rule 1, family ADR-003 decision 4) and, once
|
||||||
published, does not change. The russh version pin is a one-way door
|
published, does not change. The russh version pin is a one-way door
|
||||||
in practice (SSH key exchange/kex algorithm negotiation crosses
|
in practice (SSH key exchange/kex algorithm negotiation crosses
|
||||||
crate boundaries); bumps are deliberate, tested, and recorded.
|
crate boundaries); bumps are deliberate, tested, and recorded.
|
||||||
Protocol capability advertisement is honest: never advertise what we
|
Protocol capability advertisement is honest: never advertise what we
|
||||||
don't serve — an unfeatured channel type is declined, not advertised.
|
don't serve: an unfeatured channel type is declined, not advertised.
|
||||||
|
|
||||||
9. **Bounded resources** — every session carries wall-clock, size, and
|
9. **Bounded resources**: every session carries wall-clock, size, and
|
||||||
channel limits (max concurrent channels, max channel window, session
|
channel limits (max concurrent channels, max channel window, session
|
||||||
timeout, auth attempt limits). SSH servers are internet-facing;
|
timeout, auth attempt limits). SSH servers are internet-facing;
|
||||||
unbounded loops and unbounded buffers are bugs.
|
unbounded loops and unbounded buffers are bugs.
|
||||||
|
|
||||||
10. **License hygiene** — this project is MIT OR Apache-2.0. russh is
|
10. **License hygiene**: this project is MIT OR Apache-2.0. russh is
|
||||||
MIT/Apache-2.0-compatible; check before adopting any new dependency,
|
MIT/Apache-2.0-compatible; check before adopting any new dependency,
|
||||||
and never copy or derive code from incompatible-licensed reference
|
and never copy or derive code from incompatible-licensed reference
|
||||||
projects. Reference code is for reading and facts, never copying.
|
projects. Reference code is for reading and facts, never copying.
|
||||||
|
|
||||||
11. **Naming** — Rust standard: `snake_case` for functions/variables/
|
11. **Naming**: Rust standard: `snake_case` for functions/variables/
|
||||||
modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for
|
modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for
|
||||||
constants.
|
constants.
|
||||||
|
|
||||||
12. **Module structure** — one module per file under `src/`, re-exported
|
12. **Module structure**: one module per file under `src/`, re-exported
|
||||||
from the crate root (the alktty crate-root pattern). Public API
|
from the crate root (the alktty crate-root pattern). Public API
|
||||||
surface is `lib.rs` re-exports. The mapping table
|
surface is `lib.rs` re-exports. The mapping table
|
||||||
(channel-type → family protocol) is the crate's spine; each mapping
|
(channel-type → family protocol) is the crate's spine; each mapping
|
||||||
lives behind its feature and speaks only its family protocol's
|
lives behind its feature and speaks only its family protocol's
|
||||||
types.
|
types.
|
||||||
|
|
||||||
13. **Composability boundary** — alkssh is a door, not a backend: it
|
13. **Composability boundary**: alkssh is a door, not a backend: it
|
||||||
terminates vanilla ssh and forwards mapped channel payloads to
|
terminates vanilla ssh and forwards mapped channel payloads to
|
||||||
family protocol halves (alktty/alktunnels/alksocks/alkgit). It
|
family protocol halves (alktty/alktunnels/alksocks/alkgit). It
|
||||||
depends on alkcall types at the wire layer and the mapped crates'
|
depends on alkcall types at the wire layer and the mapped crates'
|
||||||
session/client surfaces, and carries no deployment policy — a
|
session/client surfaces, and carries no deployment policy; a
|
||||||
downstream app (alkforge) embeds it, enables the features it wants,
|
downstream app (alkforge) embeds it, enables the features it wants,
|
||||||
and brings identity/ACL configuration. No feature implies another;
|
and brings identity/ACL configuration. No feature implies another;
|
||||||
the feature graph is independent and flat until architecture says
|
the feature graph is independent and flat until architecture says
|
||||||
otherwise.
|
otherwise.
|
||||||
|
|
||||||
|
14. **No em dashes in prose.** Do not use the em dash (U+2014) in source
|
||||||
|
doc comments, markdown docs (`docs/`, `README.md`, `CHANGELOG.md`,
|
||||||
|
`AGENTS.md`), agent prompts under `.opencode/agents/`, or commit
|
||||||
|
messages. Restructure instead: split the sentence, use parentheses,
|
||||||
|
commas, a colon, or a semicolon, whichever reads
|
||||||
|
naturally, and vary the choice rather than converting every instance
|
||||||
|
the same way. A spaced hyphen (` - `) is not an acceptable
|
||||||
|
substitute; it reads as a different but equally recognizable writing
|
||||||
|
tell. The same rule covers en dashes used as clause separators,
|
||||||
|
though en dashes inside numeric ranges keep their normal meaning.
|
||||||
|
Hyphens in compound words (`fail-closed`, `client-side`) are
|
||||||
|
untouched. In test fixtures and scripts the rule applies to prose-y
|
||||||
|
content only: comment lines are rewritten too, while fixture data,
|
||||||
|
wire examples, and tool transcripts survive verbatim.
|
||||||
|
|
||||||
## Verification Commands
|
## Verification Commands
|
||||||
|
|
||||||
Run these before committing. All must pass.
|
Run these before committing. All must pass.
|
||||||
@@ -164,6 +179,9 @@ cargo clippy --all-targets -- -D warnings
|
|||||||
cargo fmt --check
|
cargo fmt --check
|
||||||
cargo doc --no-deps # if docs changed
|
cargo doc --no-deps # if docs changed
|
||||||
cargo publish --dry-run --allow-dirty # before a release
|
cargo publish --dry-run --allow-dirty # before a release
|
||||||
|
grep -rn '—' src/ docs/ README.md CHANGELOG.md AGENTS.md \
|
||||||
|
.opencode/agents/ tests/ scripts/ # must be zero hits;
|
||||||
|
# see convention 14
|
||||||
scripts/matrix.sh # the decline-matrix runner: the full suite per
|
scripts/matrix.sh # the decline-matrix runner: the full suite per
|
||||||
# feature build (none / git / session / tunnel /
|
# feature build (none / git / session / tunnel /
|
||||||
# sftp / --all-features), clippy ×2, fmt, doc
|
# sftp / --all-features), clippy ×2, fmt, doc
|
||||||
@@ -173,7 +191,7 @@ cargo test --manifest-path fuzz/shared/Cargo.toml # the fuzz corpus gate: the
|
|||||||
|
|
||||||
If feature flags are added, also run `cargo test --all-features` and
|
If feature flags are added, also run `cargo test --all-features` and
|
||||||
`cargo clippy --all-features --all-targets -- -D warnings`. Once the
|
`cargo clippy --all-features --all-targets -- -D warnings`. Once the
|
||||||
feature graph lands, each disabled feature's decline path gets a test —
|
feature graph lands, each disabled feature's decline path gets a test;
|
||||||
the fail-closed invariant is contract territory on both sides of every
|
the fail-closed invariant is contract territory on both sides of every
|
||||||
feature.
|
feature.
|
||||||
|
|
||||||
@@ -187,53 +205,53 @@ Any file under `tasks/` is a taskgraph task. Frontmatter is required
|
|||||||
isolated|component|phase|project`, `level:
|
isolated|component|phase|project`, `level:
|
||||||
planning|decomposition|implementation|review|research`. A file without
|
planning|decomposition|implementation|review|research`. A file without
|
||||||
frontmatter is silently dropped from the graph (validate does not flag
|
frontmatter is silently dropped from the graph (validate does not flag
|
||||||
it) — prose-only trackers belong under `docs/`. Run `taskgraph
|
it); prose-only trackers belong under `docs/`. Run `taskgraph
|
||||||
validate` after writing or editing anything under `tasks/`; full field
|
validate` after writing or editing anything under `tasks/`; full field
|
||||||
semantics live in `docs/sdd_process.md` §"Task File Format" /
|
semantics live in `docs/sdd_process.md` §"Task File Format" /
|
||||||
§"Categorical Estimates".
|
§"Categorical Estimates".
|
||||||
|
|
||||||
## Lifecycle Status
|
## Lifecycle Status
|
||||||
|
|
||||||
The project is in **SDD phase 0** (exploration; docs only — no
|
The project is in **SDD phase 0** (exploration; docs only, no
|
||||||
architecture committed, no implementation). Family ADR-003 (2026-10-25,
|
architecture committed, no implementation). Family ADR-003 (2026-10-25,
|
||||||
Accepted) records the structural decision already made: russh-wrapped
|
Accepted) records the structural decision already made: russh-wrapped
|
||||||
adapter, no in-family SSH framing, fail-closed capability features,
|
adapter, no in-family SSH framing, fail-closed capability features,
|
||||||
alknet-main key model via alkvault-backed sources; OQ-08's ordering
|
alknet-main key model via alkvault-backed sources; OQ-08's ordering
|
||||||
question is resolved (alkssh leads alkforge's phase 0) and OQ-08's
|
question is resolved (alkssh leads alkforge's phase 0) and OQ-08's
|
||||||
remaining scope — the channel-type → family-protocol mapping table
|
remaining scope (the channel-type → family-protocol mapping table
|
||||||
details, the auth design tying russh's authorized-key handling to
|
details, the auth design tying russh's authorized-key handling to
|
||||||
alkcall auth, and the `alk/ssh` ALPN pin (ledger rule 1) — is this
|
alkcall auth, and the `alk/ssh` ALPN pin, ledger rule 1) is this
|
||||||
repo's phase 0 work. Phase 0 output lands in `docs/research/` (the
|
repo's phase 0 work. Phase 0 output lands in `docs/research/` (the
|
||||||
phase 0 document is the current work item); `docs/architecture/` +
|
phase 0 document is the current work item); `docs/architecture/` +
|
||||||
ADR-001 (the `alk/ssh` ALPN pin) come with phase 1. POC code never
|
ADR-001 (the `alk/ssh` ALPN pin) come with phase 1. POC code never
|
||||||
merges to main — two modes: branch mode (POC builds on repo code; git
|
merges to main; two modes exist: branch mode (POC builds on repo code; git
|
||||||
branch, findings merged into research docs, branch dropped) or
|
branch, findings merged into research docs, branch dropped) or
|
||||||
standalone mode (POC independent of repo code; scratch project at
|
standalone mode (POC independent of repo code; scratch project at
|
||||||
`/workspace/<poc-name>`). POCs have relaxed constraints
|
`/workspace/<poc-name>`). POCs have relaxed constraints
|
||||||
(comments/unwrap acceptable) — they are exploration tools, not
|
(comments/unwrap acceptable) since they are exploration tools, not
|
||||||
production code.
|
production code.
|
||||||
|
|
||||||
## Architecture Context
|
## Architecture Context
|
||||||
|
|
||||||
- `docs/sdd_process.md` — the SDD process (phases, roles, task format)
|
- `docs/sdd_process.md`: the SDD process (phases, roles, task format)
|
||||||
- `docs/research/` — phase 0 research (current source of truth for
|
- `docs/research/`: phase 0 research (current source of truth for
|
||||||
design direction):
|
design direction):
|
||||||
- `vision.md` — vision, guiding principles, non-goals, the phase 0
|
- `vision.md`: vision, guiding principles, non-goals, the phase 0
|
||||||
scope questions
|
scope questions
|
||||||
- `russh.md` — russh crate inventory (server/client Handler, Config
|
- `russh.md`: russh crate inventory (server/client Handler, Config
|
||||||
knobs, keys module) and pin considerations (RC-pinned dep tree,
|
knobs, keys module) and pin considerations (RC-pinned dep tree,
|
||||||
MSRV, license) — no alknet-derived bias; the pin is verified
|
MSRV, license); no alknet-derived bias. The pin is verified
|
||||||
against current russh at architecture time
|
against current russh at architecture time
|
||||||
- `auth-bridge.md` — russh auth ↔ alkcall `IdentityProvider` mapping
|
- `auth-bridge.md`: russh auth ↔ alkcall `IdentityProvider` mapping
|
||||||
(server: verified-key fingerprint → `resolve_from_fingerprint`;
|
(server: verified-key fingerprint → `resolve_from_fingerprint`;
|
||||||
client: alkvault-backed key sources — held keys encrypted at rest,
|
client: alkvault-backed key sources, held keys encrypted at rest,
|
||||||
at-rest/bearer-token-class storage, not key derivation)
|
at-rest/bearer-token-class storage, not key derivation)
|
||||||
- `mapping-table.md` — the crate's spine: SSH channel type / request
|
- `mapping-table.md`: the crate's spine: SSH channel type / request
|
||||||
→ family protocol seam + cargo feature; the alktunnels
|
→ family protocol seam + cargo feature; the alktunnels
|
||||||
absence note; the `alk/ssh` ALPN topology question
|
absence note; the `alk/ssh` ALPN topology question
|
||||||
- `pocs.md` — POC-1..3 plan (auth bridge, client-side at-rest key
|
- `pocs.md`: POC-1..3 plan (auth bridge, client-side at-rest key
|
||||||
handling, mapping bake-off) gating phase 0 completion
|
handling, mapping bake-off) gating phase 0 completion
|
||||||
- family docs: `/workspace/@alkdev/alkfamily/docs/` —
|
- family docs: `/workspace/@alkdev/alkfamily/docs/`;
|
||||||
`architecture/decisions/003-alkssh-russh-adapter.md` is the structural
|
`architecture/decisions/003-alkssh-russh-adapter.md` is the structural
|
||||||
decision this repo builds from (and `002-alkforge-gitea-replacement.md`
|
decision this repo builds from (and `002-alkforge-gitea-replacement.md`
|
||||||
explains why: alkforge's git door, fail-closed posture);
|
explains why: alkforge's git door, fail-closed posture);
|
||||||
@@ -249,8 +267,8 @@ production code.
|
|||||||
analog for the adapter shape.
|
analog for the adapter shape.
|
||||||
- alknet `main` key model reference:
|
- alknet `main` key model reference:
|
||||||
`/workspace/@alkdev/alknet-main/crates/alknet-core/src/auth/`
|
`/workspace/@alkdev/alknet-main/crates/alknet-core/src/auth/`
|
||||||
(russh 0.49, `keys.rs` / `server_auth.rs` / `identity.rs`) — the
|
(russh 0.49, `keys.rs` / `server_auth.rs` / `identity.rs`): the
|
||||||
authorized-key/KeySource construction alkssh reuses; a reading aid
|
authorized-key/KeySource construction alkssh reuses; a reading aid
|
||||||
and correctness reference, not a path dependency.
|
and correctness reference, not a path dependency.
|
||||||
- russh: pin per phase 0 research (the alknet pin — 0.49 — is a data
|
- russh: pin per phase 0 research (the alknet pin, 0.49, is a data
|
||||||
point, not a decision).
|
point, not a decision).
|
||||||
+103
-28
@@ -4,68 +4,143 @@ All notable changes to this crate are documented here. The format is
|
|||||||
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||||
this crate adheres to [Semantic Versioning](https://semver.org/).
|
this crate adheres to [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-11-04
|
||||||
|
|
||||||
|
The sibling-adoption release: the three alkssh-filed upstream reviews
|
||||||
|
resolved across the family, and alkssh adopts the results. Two
|
||||||
|
dependency requirement bumps (the minor bump, not a patch: dep-req
|
||||||
|
changes are semver-significant for consumers) plus the review-003 W2
|
||||||
|
adoption wave. No alkssh-side wire-format change, no public API
|
||||||
|
change apart from what the dep bumps ripple through.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **alkvault 0.1 → 0.2** (breaking req): `VaultServiceHandle::decrypt`
|
||||||
|
returns `Zeroizing<String>` (alkvault review 001, consumer input
|
||||||
|
from alkssh; resolution paths 1+2). The adapter seam's own
|
||||||
|
`Zeroizing` wrap in `decrypt_and_decode` is **dropped** (it would be
|
||||||
|
a dead double wrap): the pre-wrap unfenced plaintext window ADR-005
|
||||||
|
decision 3 recorded is closed upstream; the vault fences the
|
||||||
|
plaintext from its return onward, on every path including its UTF-8
|
||||||
|
error path, and `rotate` never holds a bare plaintext either.
|
||||||
|
`decode_key_arm`'s signature is unchanged (it kept riding
|
||||||
|
`&Zeroizing<String>`; the posture now proven by the vault's return
|
||||||
|
type instead of the adapter seam).
|
||||||
|
- **alktunnels 0.4.1 → 0.5.0** (breaking req, additive API): the
|
||||||
|
review-003 W2 adoption: the forwarded serving surface
|
||||||
|
(`-R` leg's door-side arrival half) now registers
|
||||||
|
**`register_tunnel_forwarded_openable_with_establisher`** with a
|
||||||
|
`ForwardedGate` capturing the door's `ListenRouter`: a stale/missing
|
||||||
|
`listen` token fails the open **typed at the call plane**
|
||||||
|
(`channel:open_failed`, `details.reason == "unknown_resource"`)
|
||||||
|
*before any channel is born*, closing the phantom-channel shape
|
||||||
|
(call-plane success, then handler-side decline) ADR-008 Amendment 2
|
||||||
|
targets; a malformed arrival is likewise typed (`handler_error`)
|
||||||
|
instead of success-then-silent-birth-teardown. The handler-side
|
||||||
|
correlation re-lookup keeps the residual-race + cap-admission
|
||||||
|
teardown-at-birth fallback (the base shape's posture, retained).
|
||||||
|
The wire delta is empty (additive registration only); the ACL
|
||||||
|
(`tunnel:forwarded`) still runs before the establisher (now
|
||||||
|
double-pinned by the unscoped-arrival row, which uses a genuinely
|
||||||
|
minted token).
|
||||||
|
- **alktty 0.8.1 → 0.8.2** (patch): the PTY exit-signal-fidelity fix
|
||||||
|
(alktty review 004, consumer input from alkssh); the requirement
|
||||||
|
resolves to 0.8.2 automatically; the pin text refreshed. **No
|
||||||
|
alkssh contract change**: the negative-code convention (OQ-10's
|
||||||
|
derivation contract) is consumed verbatim, so PTY-row signal
|
||||||
|
deaths now report `exit-signal(name)` through the door exactly as
|
||||||
|
pipe-mode already did; OQ-10's upstream constraint (the
|
||||||
|
portable-pty flattening) is retired.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- ADR-005 decision 3 amended (the gap is closed upstream; the
|
||||||
|
adapter's obligation shrinks to consuming the zeroized value in one
|
||||||
|
pass), the `Negative` consequence amended, OQ-07 and OQ-10 amended +
|
||||||
|
the two upstream watch items closed, `mapping.md`'s `forwarded-tcpip`
|
||||||
|
row (the gate shape) and derivation-contract rule 5 (the PTY
|
||||||
|
constraint retired) updated, `client.md`'s OQ-07 anchor updated.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The test doubles' fidelity: `tests/tunnel_mapping.rs`'s harness
|
||||||
|
opener now maps the wire refusal's `details.reason` back into the
|
||||||
|
`TunnelEstablishError` taxonomy (the real opener's table) instead of
|
||||||
|
swallowing every call-plane refusal into `HandlerError`; the typed
|
||||||
|
`unknown_resource` survives the client side, which is exactly what
|
||||||
|
the new gate rows pin.
|
||||||
|
|
||||||
|
### New rows
|
||||||
|
|
||||||
|
- `a_correlation_miss_fails_the_open_typed_before_any_channel` (the
|
||||||
|
gate miss: typed refusal, no phantom channel, no acceptor row),
|
||||||
|
`a_gate_pass_arrival_serves_the_route_unchanged` (the gate-pass
|
||||||
|
posture: the serving flow is the unchanged shape), and the source
|
||||||
|
unit pin `the_pump_residual_miss_teardown_at_births_the_fabric_half`
|
||||||
|
(the handler-side residual miss stays teardown-at-birth).
|
||||||
|
|
||||||
## [0.1.0] - 2026-10-30
|
## [0.1.0] - 2026-10-30
|
||||||
|
|
||||||
The founding entry — the crate's first release. `alkssh` is the alk
|
The founding entry: the crate's first release. `alkssh` is the alk
|
||||||
family's SSH adapter (family ADR-003): a thin wrapper around russh
|
family's SSH adapter (family ADR-003): a thin wrapper around russh
|
||||||
0.65 terminating the vanilla-ssh wire and bridging to/from alkcall
|
0.65 terminating the vanilla-ssh wire and bridging to/from alkcall
|
||||||
channels — the two-direction door for vanilla clients. The wire is
|
channels, the two-direction door for vanilla clients. The wire is
|
||||||
russh's; the crate's substance is the channel-type mappings
|
russh's; the crate's substance is the channel-type mappings
|
||||||
(session/pty → alktty, direct/forwarded-tcpip → alktunnels, git exec →
|
(session/pty → alktty, direct/forwarded-tcpip → alktunnels, git exec →
|
||||||
alkgit), each behind its cargo feature in a flat, independent graph
|
alkgit), each behind its cargo feature in a flat, independent graph
|
||||||
(ADR-003): **a default build has no capabilities and serves nothing
|
(ADR-003): **a default build has no capabilities and serves nothing
|
||||||
but clean declines** — every unfeatured or unimplemented request is
|
but clean declines**. Every unfeatured or unimplemented request is
|
||||||
declined fail-closed, never half-served or silently fallen back, and
|
declined fail-closed, never half-served or silently fallen back, and
|
||||||
never advertised. Identity is the family's: a verified key's
|
never advertised. Identity is the family's: a verified key's
|
||||||
fingerprint → alkcall `IdentityProvider`; keys the crate holds come
|
fingerprint → alkcall `IdentityProvider`; keys the crate holds come
|
||||||
from alkvault-backed sources, encrypted at rest, decrypted at use,
|
from alkvault-backed sources, encrypted at rest, decrypted at use,
|
||||||
never env vars (ADR-005). The crate is a door, not a backend — no
|
never env vars (ADR-005). The crate is a door, not a backend: no
|
||||||
binary, no deployment policy; a downstream app embeds it, enables
|
binary, no deployment policy; a downstream app embeds it, enables
|
||||||
features, and brings identity/ACL configuration.
|
features, and brings identity/ACL configuration.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
**Phase 0/1 — research and architecture** (the contract the
|
**Phase 0/1, research and architecture** (the contract the
|
||||||
implementation lands):
|
implementation lands):
|
||||||
|
|
||||||
- SDD process, phase 0 research set (`docs/research/`), and the three
|
- SDD process, phase 0 research set (`docs/research/`), and the three
|
||||||
passed POCs (auth bridge, vault keys, mapping bake-off; findings in
|
passed POCs (auth bridge, vault keys, mapping bake-off; findings in
|
||||||
`poc-1..3` + `reverse-runner.md`; POC source never merged).
|
`poc-1..3` + `reverse-runner.md`; POC source never merged).
|
||||||
- Phase 1 architecture under `docs/architecture/`: the spec docs and
|
- Phase 1 architecture under `docs/architecture/`: the spec docs and
|
||||||
ADRs 001–006 (Accepted) — the `alk/ssh` ALPN pin; the russh 0.65.0
|
ADRs 001–006 (Accepted): the `alk/ssh` ALPN pin; the russh 0.65.0
|
||||||
pin (aws-lc-rs, no compression, rsa off); the capability feature
|
pin (aws-lc-rs, no compression, rsa off); the capability feature
|
||||||
graph; auth dispositions; vault-key custody; the mapping/bridge/
|
graph; auth dispositions; vault-key custody; the mapping/bridge/
|
||||||
dispatch decisions.
|
dispatch decisions.
|
||||||
|
|
||||||
**Wave 1 — foundations:**
|
**Wave 1, foundations:**
|
||||||
|
|
||||||
- The manifest: the russh 0.65 pin (`aws-lc-rs`, default features
|
- The manifest: the russh 0.65 pin (`aws-lc-rs`, default features
|
||||||
off), the flat feature graph (`session` / `git` / `tunnel` /
|
off), the flat feature graph (`session` / `git` / `tunnel` /
|
||||||
`sftp`, `default = []`).
|
`sftp`, `default = []`).
|
||||||
- The fail-closed error taxonomy (`thiserror`): `AdapterError`,
|
- The fail-closed error taxonomy (`thiserror`): `AdapterError`,
|
||||||
`DialError`, `KeySourceError`, `OpError`, `OpenDeclineReason` —
|
`DialError`, `KeySourceError`, `OpError`, `OpenDeclineReason`:
|
||||||
declines are dispositions, never panics; no `unwrap` outside tests.
|
declines are dispositions, never panics; no `unwrap` outside tests.
|
||||||
- Vault-backed key custody in code: the `KeySource` /
|
- Vault-backed key custody in code: the `KeySource` /
|
||||||
`HostKeySource` seams and the ADR-005 implementations
|
`HostKeySource` seams and the ADR-005 implementations
|
||||||
(`VaultKeySource`, `VaultHostKeySource`) — decrypt-at-use /
|
(`VaultKeySource`, `VaultHostKeySource`): decrypt-at-use /
|
||||||
decrypt-at-construction, the `Zeroizing` seam, one
|
decrypt-at-construction, the `Zeroizing` seam, one
|
||||||
`decode_secret_key` conversion.
|
`decode_secret_key` conversion.
|
||||||
- The `HostKeyPolicy` seam (OQ-05, policy-blind) with the fail-closed
|
- The `HostKeyPolicy` seam (OQ-05, policy-blind) with the fail-closed
|
||||||
default `RejectAll` — no accept-all built-in exists in the crate.
|
default `RejectAll`; no accept-all built-in exists in the crate.
|
||||||
- The contract-invariant fingerprint computation (the crate's single
|
- The contract-invariant fingerprint computation (the crate's single
|
||||||
fingerprint path, the `SHA256:` format both the auth bridge and the
|
fingerprint path, the `SHA256:` format both the auth bridge and the
|
||||||
host-key policy consume).
|
host-key policy consume).
|
||||||
- The config surface: `ServeConfig`, `DialConfig`, `Limits` (the
|
- The config surface: `ServeConfig`, `DialConfig`, `Limits` (the
|
||||||
bounded-resources budget model — wall clock always set, channel
|
bounded-resources budget model: wall clock always set, channel
|
||||||
caps, keepalive posture) — unset fields decline at request time
|
caps, keepalive posture); unset fields decline at request time
|
||||||
(OQ-08), construction never validates.
|
(OQ-08), construction never validates.
|
||||||
|
|
||||||
**Wave 2 — the server stack:**
|
**Wave 2, the server stack:**
|
||||||
|
|
||||||
- `SshAdapter` — the mount point: `SshAdapter::new(serve)` builds the
|
- `SshAdapter`, the mount point: `SshAdapter::new(serve)` builds the
|
||||||
russh server from the config (fail-closed on key-source failure);
|
russh server from the config (fail-closed on key-source failure);
|
||||||
`adapter.handle(stream)` serves one connection.
|
`adapter.handle(stream)` serves one connection.
|
||||||
- The auth bridge: russh auth hooks → ADR-004 dispositions —
|
- The auth bridge: russh auth hooks → ADR-004 dispositions:
|
||||||
verified-key fingerprint → `IdentityProvider::resolve_from_fingerprint`,
|
verified-key fingerprint → `IdentityProvider::resolve_from_fingerprint`,
|
||||||
fail-closed on every path; bounded auth attempts.
|
fail-closed on every path; bounded auth attempts.
|
||||||
- Parse-only dispatch + the fail-closed decline core: every unroutable
|
- Parse-only dispatch + the fail-closed decline core: every unroutable
|
||||||
@@ -78,12 +153,12 @@ implementation lands):
|
|||||||
caps (refuse-never-queue) and listener caps.
|
caps (refuse-never-queue) and listener caps.
|
||||||
- The in-process loopback harness (the contract-test vehicle).
|
- The in-process loopback harness (the contract-test vehicle).
|
||||||
|
|
||||||
**Wave 3 — the server mappings (the spine):**
|
**Wave 3, the server mappings (the spine):**
|
||||||
|
|
||||||
- Per-feature seam handles (`SessionSeam`, `GitSeam`,
|
- Per-feature seam handles (`SessionSeam`, `GitSeam`,
|
||||||
`TunnelListenSeam`): unset seam → decline even with the feature on.
|
`TunnelListenSeam`): unset seam → decline even with the feature on.
|
||||||
- Decline-path tracing (`tracing` events on every decline class).
|
- Decline-path tracing (`tracing` events on every decline class).
|
||||||
- The git mapping (exec-only, ADR-006): the full exec grammar —
|
- The git mapping (exec-only, ADR-006): the full exec grammar,
|
||||||
`git-{upload,receive}-pack '/<repo>'` parsed and dispatched to
|
`git-{upload,receive}-pack '/<repo>'` parsed and dispatched to
|
||||||
alkgit; a subsystem request can never carry the repository and
|
alkgit; a subsystem request can never carry the repository and
|
||||||
declines; `1` is honestly reported for push `ng`.
|
declines; `1` is honestly reported for push `ng`.
|
||||||
@@ -92,45 +167,45 @@ implementation lands):
|
|||||||
ttymodes JSON, the exit-chunk → SSH exit derivation (rules 1–6),
|
ttymodes JSON, the exit-chunk → SSH exit derivation (rules 1–6),
|
||||||
control rows (window-change, signal) on a bounded per-channel
|
control rows (window-change, signal) on a bounded per-channel
|
||||||
surface.
|
surface.
|
||||||
- Tunnel leg 1: the route-1 bridge — `direct-tcpip` /
|
- Tunnel leg 1, the route-1 bridge: `direct-tcpip` /
|
||||||
`direct-streamlocal` opens ride the assembly's `TunnelSession` over
|
`direct-streamlocal` opens ride the assembly's `TunnelSession` over
|
||||||
the family fabric; fabric failure is a clean `ConnectFailed` reject.
|
the family fabric; fabric failure is a clean `ConnectFailed` reject.
|
||||||
- Tunnel leg 2: the `-R` listen flow — `tcpip_forward` /
|
- Tunnel leg 2, the `-R` listen flow: `tcpip_forward` /
|
||||||
`streamlocal_forward` dial the fabric, arrivals correlate through
|
`streamlocal_forward` dial the fabric, arrivals correlate through
|
||||||
the token router (miss → teardown-at-birth), the data leg opens
|
the token router (miss → teardown-at-birth), the data leg opens
|
||||||
`forwarded-tcpip`/`forwarded-streamlocal` back toward the client,
|
`forwarded-tcpip`/`forwarded-streamlocal` back toward the client,
|
||||||
port-0 mints echo the actually-bound port.
|
port-0 mints echo the actually-bound port.
|
||||||
|
|
||||||
**Wave 4 — the client direction:**
|
**Wave 4, the client direction:**
|
||||||
|
|
||||||
- The dial surface: `dial(DialConfig)` — key load (decrypt-at-dial),
|
- The dial surface: `dial(DialConfig)`: key load (decrypt-at-dial),
|
||||||
bounded russh client config, the host-key policy call, exactly one
|
bounded russh client config, the host-key policy call, exactly one
|
||||||
publickey attempt; `SshConnection` the handle.
|
publickey attempt; `SshConnection` the handle.
|
||||||
- The op rows: `exec` / `pty_exec` / `shell` (the alktty chunk plane),
|
- The op rows: `exec` / `pty_exec` / `shell` (the alktty chunk plane),
|
||||||
`tunnel_direct` / `tunnel_direct_unix` (byte duplex),
|
`tunnel_direct` / `tunnel_direct_unix` (byte duplex),
|
||||||
`git_service` (pack duplex + consumed exit) — plus the `sftp`
|
`git_service` (pack duplex + consumed exit), plus the `sftp`
|
||||||
decline row (always declines, every build).
|
decline row (always declines, every build).
|
||||||
- The `-R` leg: `reverse_listen` / `reverse_listen_unix` + the
|
- The `-R` leg: `reverse_listen` / `reverse_listen_unix` + the
|
||||||
`AcceptorRegistry` (refuse-never-replace) and the acceptor traits —
|
`AcceptorRegistry` (refuse-never-replace) and the acceptor traits:
|
||||||
the hub asks the vanilla server to listen and serves the accepted
|
the hub asks the vanilla server to listen and serves the accepted
|
||||||
channels.
|
channels.
|
||||||
- Lifecycle primitives (OQ-12): explicitly cancellable listener
|
- Lifecycle primitives (OQ-12): explicitly cancellable listener
|
||||||
handles, clean `close()` (`Disconnect::ByApplication`) — drop is
|
handles, clean `close()` (`Disconnect::ByApplication`); drop is
|
||||||
never the clean path at this pin; no reconnect/restore policy (the
|
never the clean path at this pin; no reconnect/restore policy (the
|
||||||
crate ships primitives, the assembly composes).
|
crate ships primitives, the assembly composes).
|
||||||
- Hardening riders: the git row's status-before-close by construction,
|
- Hardening riders: the git row's status-before-close by construction,
|
||||||
the channel-not-held guard wire rows, tty control-row coverage, the
|
the channel-not-held guard wire rows, tty control-row coverage, the
|
||||||
`ListenRouter` token-collision pre-check.
|
`ListenRouter` token-collision pre-check.
|
||||||
|
|
||||||
**Wave 5 — validation and release:**
|
**Wave 5, validation and release:**
|
||||||
|
|
||||||
- The decline-matrix contract suite: every mapping-decline row × every
|
- The decline-matrix contract suite: every mapping-decline row × every
|
||||||
build it can appear in × both directions (the matrix runner
|
build it can appear in × both directions (the matrix runner
|
||||||
`scripts/matrix.sh` — one command over all feature builds).
|
`scripts/matrix.sh`, one command over all feature builds).
|
||||||
- Real-tool e2e both directions: OpenSSH (pty/shell/exec, `-L`, `-R`,
|
- Real-tool e2e both directions: OpenSSH (pty/shell/exec, `-L`, `-R`,
|
||||||
`-D` + curl), git clone and push through the door, and a real
|
`-D` + curl), git clone and push through the door, and a real
|
||||||
`sshd` against the client direction (exec, direct-tcpip dial, `-R`
|
`sshd` against the client direction (exec, direct-tcpip dial, `-R`
|
||||||
listen + acceptor) — check-gated, skipped rows carry reasons.
|
listen + acceptor), check-gated; skipped rows carry reasons.
|
||||||
- The `fuzz/` sidecar: the ingress surfaces (exec parse grammar,
|
- The `fuzz/` sidecar: the ingress surfaces (exec parse grammar,
|
||||||
fingerprint format, key decode, `TerminalParamsWire`) with the
|
fingerprint format, key decode, `TerminalParamsWire`) with the
|
||||||
committed-seed corpus gate (`cargo test --manifest-path
|
committed-seed corpus gate (`cargo test --manifest-path
|
||||||
@@ -141,7 +216,7 @@ implementation lands):
|
|||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
- `sftp` is a feature of decline: enabling it adds no capability —
|
- `sftp` is a feature of decline: enabling it adds no capability;
|
||||||
the `sftp` subsystem request still declines in every build until
|
the `sftp` subsystem request still declines in every build until
|
||||||
the alkfs mapping lands (OQ-13; the decline stays contract
|
the alkfs mapping lands (OQ-13; the decline stays contract
|
||||||
territory).
|
territory).
|
||||||
|
|||||||
Generated
+7
-7
@@ -109,7 +109,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alkssh"
|
name = "alkssh"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alkcall",
|
"alkcall",
|
||||||
"alkgit",
|
"alkgit",
|
||||||
@@ -131,9 +131,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alktty"
|
name = "alktty"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2265167842b7d3ea09bc235f48450d3dcf22049a2ca71f8155de00acbf97c226"
|
checksum = "9cab9b0d3d6c1bdef2622799e3b95bb6063720153a124a57f4c60f95f3d42ea8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alkcall",
|
"alkcall",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -153,9 +153,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alktunnels"
|
name = "alktunnels"
|
||||||
version = "0.4.1"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ee56da894f2d02120d3602df32c7f6a9db7f930a401df66d40b632cd9767af41"
|
checksum = "198d3aa766889cf5cc9b764319f9aa75e21234d623021893c26675f7c4276242"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alkcall",
|
"alkcall",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -169,9 +169,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alkvault"
|
name = "alkvault"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52e032b074f7fea71ea15528994c5b3a6503c082c519045bb3b0519281291918"
|
checksum = "b2522ded10a506770779e72063422057bb63f1b75457c1742a3362072adfb1c4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+46
-34
@@ -1,6 +1,6 @@
|
|||||||
# Single-crate protocol/door crate (the alktty template). No workspace:
|
# Single-crate protocol/door crate (the alktty template). No workspace:
|
||||||
# the composition story is deferred — alkssh's substance is the mappings
|
# the composition story is deferred: alkssh's substance is the mappings
|
||||||
# (family ADR-003). The `fuzz/` sidecar is its own workspace (excluded —
|
# (family ADR-003). The `fuzz/` sidecar is its own workspace (excluded:
|
||||||
# it pins its own nightly toolchain and never rides the root's).
|
# it pins its own nightly toolchain and never rides the root's).
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["."]
|
members = ["."]
|
||||||
@@ -8,18 +8,18 @@ exclude = ["fuzz"]
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "alkssh"
|
name = "alkssh"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.89"
|
rust-version = "1.89"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
description = "SSH adapter for the alk family: russh-wrapped vanilla-ssh termination bridged to/from alkcall channels — session/pty, tunnels, git exec — with every capability mapping a fail-closed cargo feature (no features: no capabilities)"
|
description = "SSH adapter for the alk family: russh-wrapped vanilla-ssh termination bridged to/from alkcall channels (session/pty, tunnels, git exec), with every capability mapping a fail-closed cargo feature (no features: no capabilities)"
|
||||||
repository = "https://git.alk.dev/alkdev/alkssh"
|
repository = "https://git.alk.dev/alkdev/alkssh"
|
||||||
keywords = ["ssh", "russh", "protocol", "alkcall", "network"]
|
keywords = ["ssh", "russh", "protocol", "alkcall", "network"]
|
||||||
categories = ["network-programming", "asynchronous"]
|
categories = ["network-programming", "asynchronous"]
|
||||||
# The package policy (the sibling crates' convention): the published crate
|
# The package policy (the sibling crates' convention): the published crate
|
||||||
# ships the architecture docs (the authoritative contract) but not the
|
# ships the architecture docs (the authoritative contract) but not the
|
||||||
# process surface — reviews, research, plans, task files, the working
|
# process surface: reviews, research, plans, task files, the working
|
||||||
# agents' config, AGENTS.md — and not the `fuzz/` sidecar (its own
|
# agents' config, AGENTS.md; and not the `fuzz/` sidecar (its own
|
||||||
# workspace; never a root dep, never a shipped surface).
|
# workspace; never a root dep, never a shipped surface).
|
||||||
exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/", "docs/research/", "docs/sdd_process.md", "tasks/", "fuzz/"]
|
exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/", "docs/research/", "docs/sdd_process.md", "tasks/", "fuzz/"]
|
||||||
|
|
||||||
@@ -27,12 +27,12 @@ exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/", "docs/rese
|
|||||||
name = "alkssh"
|
name = "alkssh"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# ADR-003: flat and independent — no feature implies another; `default = []`
|
# ADR-003: flat and independent, no feature implies another; `default = []`
|
||||||
# by design (a default build has no capabilities; every unfeatured open
|
# by design (a default build has no capabilities; every unfeatured open
|
||||||
# declines fail-closed). The wave-2 decline core is the first `cfg`
|
# declines fail-closed). The wave-2 decline core is the first `cfg`
|
||||||
# consumer; the wave-3 seam-config handles are the second (each optional
|
# consumer; the wave-3 seam-config handles are the second (each optional
|
||||||
# sibling dep is gated feature-to-dep so a git-build never compiles
|
# sibling dep is gated feature-to-dep so a git-build never compiles
|
||||||
# alktty and vice versa — the wave-1 manifest rule: per-feature sibling
|
# alktty and vice versa, per the wave-1 manifest rule: per-feature sibling
|
||||||
# deps land with their first consuming wave, which for alktty/alkgit is
|
# deps land with their first consuming wave, which for alktty/alkgit is
|
||||||
# the wave-3 config fields and for alktunnels `w3-tunnel-direct`, the
|
# the wave-3 config fields and for alktunnels `w3-tunnel-direct`, the
|
||||||
# route-1 bridge).
|
# route-1 bridge).
|
||||||
@@ -44,38 +44,50 @@ sftp = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# async-trait (the alkcall precedent) makes the injected handles
|
# async-trait (the alkcall precedent) makes the injected handles
|
||||||
# dyn-compatible (`Arc<dyn ...>`) — the HostKeyPolicy dial handle needs
|
# dyn-compatible (`Arc<dyn ...>`); the HostKeyPolicy dial handle needs
|
||||||
# that, and native AFIT traits are not object-safe at this toolchain.
|
# that, and native AFIT traits are not object-safe at this toolchain.
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
# ADR-002: aws-lc-rs only; default features off — no flate2, no rsa
|
# ADR-002: aws-lc-rs only; default features off, meaning no flate2 and
|
||||||
# (ed25519-only; the tree stays RC-free except the ssh-key pin riding the
|
# no rsa (ed25519-only; the tree stays RC-free except the ssh-key pin riding the
|
||||||
# russh keys path). Never enable russh's `des`/`dsa` features.
|
# russh keys path). Never enable russh's `des`/`dsa` features.
|
||||||
russh = { version = "0.65", default-features = false, features = ["aws-lc-rs"] }
|
russh = { version = "0.65", default-features = false, features = ["aws-lc-rs"] }
|
||||||
alkcall = "0.10"
|
alkcall = "0.10"
|
||||||
alkvault = "0.1"
|
# alkvault 0.2 (breaking req bump, 0.1 -> 0.2, 2026-11-04): `decrypt`
|
||||||
|
# returns `Zeroizing<String>` (alkvault review 001's resolution; the
|
||||||
|
# pre-wrap plaintext window ADR-005 decision 3 recorded is closed
|
||||||
|
# upstream); the adapter-side wrap is dropped. Pin re-verified against
|
||||||
|
# crates.io: MIT OR Apache-2.0.
|
||||||
|
alkvault = "0.2"
|
||||||
# Wave-3 seam-handle types (`config.rs`'s `SessionSeam`/`GitSeam`).
|
# Wave-3 seam-handle types (`config.rs`'s `SessionSeam`/`GitSeam`).
|
||||||
# Pins verified against crates.io (2026-10-28): alktty 0.8.1, alkgit
|
# Pins verified against crates.io (2026-11-04): alktty 0.8.2 (0.1.0's
|
||||||
# 0.1.0 — both MIT OR Apache-2.0, both rust_version 1.88 ≤ our 1.89,
|
# 0.8.1 pin refreshed; 0.8.2 is the PTY exit-signal-fidelity fix,
|
||||||
# both over alkcall ^0.10 (shared with our `alkcall = "0.10"` → 0.10.1
|
# patch-level, API/wire unchanged; alkssh consumes negative exit
|
||||||
# resolution). alktty's `local` feature stays off — alkssh never
|
# codes verbatim so the derivation contract needed no change), alkgit
|
||||||
# touches a real backend; the `local` shapes stay alktty-side.
|
# 0.1.0: both MIT OR Apache-2.0, both rust_version ≤ our 1.89,
|
||||||
alktty = { version = "0.8.1", default-features = false, optional = true }
|
# both over alkcall ^0.10 (shared with our `alkcall = "0.10"` →
|
||||||
|
# 0.10.1 resolution). alktty's `local` feature stays off; alkssh
|
||||||
|
# never touches a real backend; the `local` shapes stay alktty-side.
|
||||||
|
alktty = { version = "0.8.2", default-features = false, optional = true }
|
||||||
alkgit = { version = "0.1.0", default-features = false, optional = true }
|
alkgit = { version = "0.1.0", default-features = false, optional = true }
|
||||||
# tunnel-only: the route-1 bridge's consumer surface (`TunnelSession::
|
# tunnel-only: the route-1 bridge's consumer surface (`TunnelSession::
|
||||||
# open_direct`/`take_halves`, `SubstrateAddr`/`Substrate`). Pin
|
# open_direct`/`take_halves`, `SubstrateAddr`/`Substrate`) + the
|
||||||
# re-verified against crates.io at `w3-tunnel-direct` (2026-10-29):
|
# forwarded serving surface's establisher shape
|
||||||
# 0.4.1 is `max_stable_version`/`newest_version`, MIT OR Apache-2.0,
|
# (`register_tunnel_forwarded_openable_with_establisher` +
|
||||||
# rust_version 1.88 ≤ our 1.89, alkcall ^0.10.1 pinned exactly
|
# `ForwardedGate`, the review-003 W2 adoption, ADR-008 Amendment 2's
|
||||||
# (single-versioned against our `alkcall = "0.10"`). The `local`
|
# recommended shape). Pin re-verified against crates.io (2026-11-04):
|
||||||
# feature stays off — the dial seam is injected (`DialFn`); the real
|
# 0.5.0 is newest_version, additive API only over 0.4.1 (the wire
|
||||||
# dial/backends belong to the assembly, and the tests hand-roll their
|
# delta is empty, the base registration is byte-for-byte), MIT OR
|
||||||
# own dial/dial-fail seams (`tests/tunnel_mapping.rs`).
|
# Apache-2.0, rust_version 1.88 ≤ our 1.89, alkcall ^0.10.1 pinned
|
||||||
alktunnels = { version = "0.4.1", default-features = false, optional = true }
|
# exactly (single-versioned against our `alkcall = "0.10"`). The
|
||||||
|
# `local` feature stays off (the dial seam is injected `DialFn`);
|
||||||
|
# the real dial/backends belong to the assembly, and the tests
|
||||||
|
# hand-roll their own dial/dial-fail seams (`tests/tunnel_mapping.rs`).
|
||||||
|
alktunnels = { version = "0.5.0", default-features = false, optional = true }
|
||||||
# session-only: the OQ-44 reserved ttymodes JSON (`TerminalParamsWire.modes`)
|
# session-only: the OQ-44 reserved ttymodes JSON (`TerminalParamsWire.modes`)
|
||||||
# and the exit-chunk / negotiation-error-frame JSON reads in `tty_map.rs`.
|
# and the exit-chunk / negotiation-error-frame JSON reads in `tty_map.rs`.
|
||||||
# Same pin alktty carries (crate resolution stays single-versioned).
|
# Same pin alktty carries (crate resolution stays single-versioned).
|
||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
# session-only: the alktty chunk plane is `bytes`-based (`Chunk.bytes`) —
|
# session-only: the alktty chunk plane is `bytes`-based (`Chunk.bytes`),
|
||||||
# the bridge builds sentinel/ctrl chunks with it. Same pin russh/alktty
|
# the bridge builds sentinel/ctrl chunks with it. Same pin russh/alktty
|
||||||
# carry (crate resolution stays single-versioned).
|
# carry (crate resolution stays single-versioned).
|
||||||
bytes = { version = "1", optional = true }
|
bytes = { version = "1", optional = true }
|
||||||
@@ -87,24 +99,24 @@ zeroize = "1.9"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# The git-seam test doubles name `GitPackGen`'s `gix_hash::ObjectId`
|
# The git-seam test doubles name `GitPackGen`'s `gix_hash::ObjectId`
|
||||||
# parameters and alkgit does not re-export them — test-graph only,
|
# parameters and alkgit does not re-export them (test-graph only),
|
||||||
# version matched to alkgit's `^0.28` req (same crate in the lockfile).
|
# version matched to alkgit's `^0.28` req (same crate in the lockfile).
|
||||||
gix-hash = { version = "0.28", features = ["sha1"] }
|
gix-hash = { version = "0.28", features = ["sha1"] }
|
||||||
# The session shim's scripted stdout/stderr streams ride tokio's
|
# The session shim's scripted stdout/stderr streams ride tokio's
|
||||||
# `ReceiverStream` — test-graph only (the crate proper hand-rolls no
|
# `ReceiverStream`, test-graph only (the crate proper hand-rolls no
|
||||||
# streams).
|
# streams).
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
# The leg-2 fabric double's establisher/params serialization
|
# The leg-2 fabric double's establisher/params serialization
|
||||||
# (`w3-tunnel-listen`) — test-graph only (the door's own lib code
|
# (`w3-tunnel-listen`), test-graph only (the door's own lib code
|
||||||
# stays serde-free under the `tunnel` feature; the double's
|
# stays serde-free under the `tunnel` feature; the double's
|
||||||
# dynamic-bind producer serializes params/reply fields).
|
# dynamic-bind producer serializes params/reply fields).
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
# The real-tool pty smoke (`w3-tty-session`): alktty's `local` backend
|
# The real-tool pty smoke (`w3-tty-session`): alktty's `local` backend
|
||||||
# composition — the dev-dep `features = ["local"]` unification applies
|
# composition: the dev-dep `features = ["local"]` unification applies
|
||||||
# to the TEST graph only (feature unification folds `local` into the
|
# to the TEST graph only (feature unification folds `local` into the
|
||||||
# optional alktty dep in test builds; the published lib never enables
|
# optional alktty dep in test builds; the published lib never enables
|
||||||
# it and `src/` never names a local shape). Same 0.8.1 pin.
|
# it and `src/` never names a local shape). Same 0.8.2 pin.
|
||||||
alktty = { version = "0.8.1", default-features = false, features = ["local"] }
|
alktty = { version = "0.8.2", default-features = false, features = ["local"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
@@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
SSH adapter for the alk family: a thin wrapper around
|
SSH adapter for the alk family: a thin wrapper around
|
||||||
[russh](https://docs.rs/russh) terminating the vanilla-ssh wire and
|
[russh](https://docs.rs/russh) terminating the vanilla-ssh wire and
|
||||||
bridging to/from alkcall channels — the SSH counterpart of the
|
bridging to/from alkcall channels, the SSH counterpart of the
|
||||||
alkhttp adapter (`family` ADR-003). Vanilla clients terminate vanilla
|
alkhttp adapter (`family` ADR-003). Vanilla clients terminate vanilla
|
||||||
ssh; family protocol halves (alktty, alktunnels, alkgit) receive their
|
ssh; family protocol halves (alktty, alktunnels, alkgit) receive their
|
||||||
own chunk planes; the door itself speaks no family protocol and owns
|
own chunk planes; the door itself speaks no family protocol and owns
|
||||||
no deployment policy.
|
no deployment policy.
|
||||||
|
|
||||||
The crate is a **door, not a backend**: no binary, no front doors — a
|
The crate is a **door, not a backend**: no binary, no front doors; a
|
||||||
downstream app (alkforge) embeds it, enables the features it wants,
|
downstream app (alkforge) embeds it, enables the features it wants,
|
||||||
and brings identity/ACL configuration and the family fabric handles.
|
and brings identity/ACL configuration and the family fabric handles.
|
||||||
It is two-directional: a server mount serves vanilla ssh into the
|
It is two-directional: a server mount serves vanilla ssh into the
|
||||||
family, and a dial points family ops at a vanilla `sshd`.
|
family, and a dial points family ops at a vanilla `sshd`.
|
||||||
|
|
||||||
## The feature graph — fail-closed
|
## The fail-closed feature graph
|
||||||
|
|
||||||
`default = []` by design: **a default build has no capabilities and
|
`default = []` by design: **a default build has no capabilities and
|
||||||
can serve nothing but clean declines.** Every channel-type mapping is
|
can serve nothing but clean declines.** Every channel-type mapping is
|
||||||
@@ -26,14 +26,14 @@ implies another):
|
|||||||
| `session` | pty / shell / exec (pipe) rows → chunk-framed session | alktty |
|
| `session` | pty / shell / exec (pipe) rows → chunk-framed session | alktty |
|
||||||
| `git` | the exec-only git rows (`git-{upload,receive}-pack`) | alkgit |
|
| `git` | the exec-only git rows (`git-{upload,receive}-pack`) | alkgit |
|
||||||
| `tunnel` | `direct-tcpip` / `direct-streamlocal`, the `-R` legs | alktunnels |
|
| `tunnel` | `direct-tcpip` / `direct-streamlocal`, the `-R` legs | alktunnels |
|
||||||
| `sftp` | reserved decline placeholder (OQ-13) — always declines until the alkfs mapping lands | — |
|
| `sftp` | reserved decline placeholder (OQ-13), always declines until the alkfs mapping lands | n/a |
|
||||||
|
|
||||||
Non-mapped surface (x11, `auth-agent`, the `env` request, git *subsystem*
|
Non-mapped surface (x11, `auth-agent`, the `env` request, git *subsystem*
|
||||||
requests, `sftp` until alkfs) declines fail-closed in every build:
|
requests, `sftp` until alkfs) declines fail-closed in every build:
|
||||||
never a half-capability, never a silent fallback, and never advertised
|
never a half-capability, never a silent fallback, and never advertised;
|
||||||
— an unfeatured channel type is declined, not advertised. A request
|
an unfeatured channel type is declined, not advertised. A request
|
||||||
whose config fields are unset declines too (an unset seam or shell
|
whose config fields are unset declines too (an unset seam or shell
|
||||||
argv configures nothing — the feature alone is not a capability).
|
argv configures nothing; the feature alone is not a capability).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ The assembly injects the seams: an alkcall
|
|||||||
the per-feature family handles. The examples below are the crate's
|
the per-feature family handles. The examples below are the crate's
|
||||||
compile-checked doctests (see the crate docs in `src/lib.rs`).
|
compile-checked doctests (see the crate docs in `src/lib.rs`).
|
||||||
|
|
||||||
A server mount — per accepted TCP connection, run the door:
|
A server mount (per accepted TCP connection, run the door):
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -77,7 +77,7 @@ let adapter = alkssh::SshAdapter::new(serve).await?;
|
|||||||
// adapter.handle(stream).await?;
|
// adapter.handle(stream).await?;
|
||||||
```
|
```
|
||||||
|
|
||||||
A dial — family ops pointed upstream at a vanilla server:
|
A dial, family ops pointed upstream at a vanilla server:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -102,18 +102,18 @@ let conn = alkssh::dial(alkssh::DialConfig::new(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// op rows (under their features): conn.exec, conn.tunnel_direct,
|
// op rows (under their features): conn.exec, conn.tunnel_direct,
|
||||||
// conn.git_service, conn.reverse_listen — see the crate docs.
|
// conn.git_service, conn.reverse_listen; see the crate docs.
|
||||||
conn.close().await; // clean disconnect — drop is never the clean path
|
conn.close().await; // clean disconnect; drop is never the clean path
|
||||||
```
|
```
|
||||||
|
|
||||||
Minimal builds compile and doctest with no features enabled —
|
Minimal builds compile and doctest with no features enabled:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ cargo build
|
$ cargo build
|
||||||
$ cargo test
|
$ cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
— and every capability row additionally rides its feature build (the
|
Every capability row additionally rides its feature build (the
|
||||||
matrix: no features / `git` / `session` / `tunnel` / `sftp` /
|
matrix: no features / `git` / `session` / `tunnel` / `sftp` /
|
||||||
`--all-features`).
|
`--all-features`).
|
||||||
|
|
||||||
@@ -122,22 +122,22 @@ matrix: no features / `git` / `session` / `tunnel` / `sftp` /
|
|||||||
- **Keys are sources, never static material** (alknet-main's `main`
|
- **Keys are sources, never static material** (alknet-main's `main`
|
||||||
key model, ADR-005): the dial/host key seams are
|
key model, ADR-005): the dial/host key seams are
|
||||||
decrypt-at-use handles over an injected
|
decrypt-at-use handles over an injected
|
||||||
[`alkvault`](https://docs.rs/alkvault) service — keys held
|
[`alkvault`](https://docs.rs/alkvault) service; keys are held
|
||||||
encrypted at rest, decrypted at use, zeroized through the seam.
|
encrypted at rest, decrypted at use, zeroized through the seam.
|
||||||
The vault is a secret store on this path, not a key generator.
|
The vault is a secret store on this path, not a key generator.
|
||||||
- **No env vars, no config-file key material, no secrets on the
|
- **No env vars, no config-file key material, no secrets on the
|
||||||
wire.** The standing family posture (family ADR-002 decision 5).
|
wire.** The standing family posture (family ADR-002 decision 5).
|
||||||
- **Publickey-only authentication**, exactly one attempt — no
|
- **Publickey-only authentication**, exactly one attempt; no
|
||||||
password, keyboard-interactive, cert, or fallback methods exist on
|
password, keyboard-interactive, cert, or fallback methods exist on
|
||||||
the surface.
|
the surface.
|
||||||
- **Host-key trust is assembly policy**: the crate ships a
|
- **Host-key trust is assembly policy**: the crate ships a
|
||||||
`HostKeyPolicy` seam with a fail-closed default (`RejectAll` — a
|
`HostKeyPolicy` seam with a fail-closed default (`RejectAll`; a
|
||||||
dial with it connects to nothing) and **no accept-all built-in**;
|
dial with it connects to nothing) and **no accept-all built-in**;
|
||||||
any permissive policy is the assembly's to write.
|
any permissive policy is the assembly's to write.
|
||||||
- **Injected identity**: every wire-exposed op is checked against
|
- **Injected identity**: every wire-exposed op is checked against
|
||||||
alkcall's `AccessControl::check(peer_identity)`; internal ops are
|
alkcall's `AccessControl::check(peer_identity)`; internal ops are
|
||||||
never wire-callable; there is no unauthenticated capability.
|
never wire-callable; there is no unauthenticated capability.
|
||||||
- **Bounded resources everywhere** (`Limits` — wall clock always
|
- **Bounded resources everywhere** (`Limits`: wall clock always
|
||||||
set, channel caps, listener caps, window/packet budgets, auth
|
set, channel caps, listener caps, window/packet budgets, auth
|
||||||
attempt limit): the crate never constructs an unbounded session.
|
attempt limit): the crate never constructs an unbounded session.
|
||||||
Known, recorded limits ride the published docs (the crate-root
|
Known, recorded limits ride the published docs (the crate-root
|
||||||
@@ -149,10 +149,10 @@ matrix: no features / `git` / `session` / `tunnel` / `sftp` /
|
|||||||
|
|
||||||
- Crate docs: `cargo doc --open` (the crate-root docs carry the
|
- Crate docs: `cargo doc --open` (the crate-root docs carry the
|
||||||
feature graph and the known-limits records).
|
feature graph and the known-limits records).
|
||||||
- `docs/architecture/` — the authoritative contract: the spec
|
- `docs/architecture/`: the authoritative contract: the spec
|
||||||
(`overview.md`, `mapping.md`, `client.md`, `auth.md`), ADRs
|
(`overview.md`, `mapping.md`, `client.md`, `auth.md`), ADRs
|
||||||
001–006, and the open-questions tracker.
|
001–006, and the open-questions tracker.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT OR Apache-2.0 — see `LICENSE-MIT` / `LICENSE-APACHE`.
|
MIT OR Apache-2.0; see `LICENSE-MIT` / `LICENSE-APACHE`.
|
||||||
+16
-16
@@ -7,12 +7,12 @@ last_updated: 2026-10-28
|
|||||||
|
|
||||||
## Current State
|
## Current State
|
||||||
|
|
||||||
Phase 1 (architecture) — complete pending implementation. Phase 0 is
|
Phase 1 (architecture) is complete pending implementation. Phase 0 is
|
||||||
closed: all three POCs passed (121 checks;
|
closed: all three POCs passed (121 checks;
|
||||||
`docs/research/poc-{1,2,3}-*.md`) and the research docs record their
|
`docs/research/poc-{1,2,3}-*.md`) and the research docs record their
|
||||||
corrections. The ADR set (001–006) is **Accepted** — including the
|
corrections. The ADR set (001–006) is **Accepted**, including the
|
||||||
post-review amendments (ADR-002's ed25519-only revert; ADR-003/006's
|
post-review amendments (ADR-002's ed25519-only revert; ADR-003/006's
|
||||||
`dynamic-forward` retirement) — and the four spec docs
|
`dynamic-forward` retirement), and the four spec docs
|
||||||
(overview/mapping/client/auth) are `reviewed` (2026-10-28): the OQ
|
(overview/mapping/client/auth) are `reviewed` (2026-10-28): the OQ
|
||||||
resolution pass closed all thirteen open questions (OQ-08/11 decided,
|
resolution pass closed all thirteen open questions (OQ-08/11 decided,
|
||||||
OQ-10 converted to a derivation contract + upstream constraint,
|
OQ-10 converted to a derivation contract + upstream constraint,
|
||||||
@@ -24,7 +24,7 @@ pass came back clean. Implementation may begin.
|
|||||||
| Document | Status | Scope |
|
| Document | Status | Scope |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| [overview.md](overview.md) | reviewed | Crate purpose, family position, surface categories, bounded-resources policy, non-goals |
|
| [overview.md](overview.md) | reviewed | Crate purpose, family position, surface categories, bounded-resources policy, non-goals |
|
||||||
| [mapping.md](mapping.md) | reviewed | The spine — server-direction rows, dispatch, spawn-and-return, decline matrix |
|
| [mapping.md](mapping.md) | reviewed | The spine: server-direction rows, dispatch, spawn-and-return, decline matrix |
|
||||||
| [client.md](client.md) | reviewed | Client-direction surface: dial, op rows, the acceptor, lifecycle primitives |
|
| [client.md](client.md) | reviewed | Client-direction surface: dial, op rows, the acceptor, lifecycle primitives |
|
||||||
| [auth.md](auth.md) | reviewed | The auth bridge (server) + custody & host-key policy (client) |
|
| [auth.md](auth.md) | reviewed | The auth bridge (server) + custody & host-key policy (client) |
|
||||||
| [open-questions.md](open-questions.md) | reviewed | Centralized OQ tracker (all OQ-01…13 resolved) |
|
| [open-questions.md](open-questions.md) | reviewed | Centralized OQ tracker (all OQ-01…13 resolved) |
|
||||||
@@ -33,24 +33,24 @@ pass came back clean. Implementation may begin.
|
|||||||
|
|
||||||
| ADR | Title | Status |
|
| ADR | Title | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| [ADR-001](decisions/001-alkssh-alpn-pin.md) | The `alk/ssh` ALPN pin — edge-termination contract for `SshAdapter` | Accepted |
|
| [ADR-001](decisions/001-alkssh-alpn-pin.md) | The `alk/ssh` ALPN pin: edge-termination contract for `SshAdapter` | Accepted |
|
||||||
| [ADR-002](decisions/002-russh-pin.md) | The russh pin — 0.65.0, aws-lc-rs, no compression, ed25519-only | Accepted |
|
| [ADR-002](decisions/002-russh-pin.md) | The russh pin (0.65.0, aws-lc-rs, no compression, ed25519-only) | Accepted |
|
||||||
| [ADR-003](decisions/003-feature-graph.md) | The capability feature graph — flat, independent, two-direction | Accepted |
|
| [ADR-003](decisions/003-feature-graph.md) | The capability feature graph: flat, independent, two-direction | Accepted |
|
||||||
| [ADR-004](decisions/004-auth-dispositions.md) | Auth dispositions — publickey-only, fail-closed, fingerprint identity | Accepted |
|
| [ADR-004](decisions/004-auth-dispositions.md) | Auth dispositions: publickey-only, fail-closed, fingerprint identity | Accepted |
|
||||||
| [ADR-005](decisions/005-at-rest-key-custody.md) | At-rest key custody — vault-wrapped OpenSSH-PEM, decrypt-at-use | Accepted |
|
| [ADR-005](decisions/005-at-rest-key-custody.md) | At-rest key custody: vault-wrapped OpenSSH-PEM, decrypt-at-use | Accepted |
|
||||||
| [ADR-006](decisions/006-mapping-bridge-and-dispatch.md) | Mapping decisions — route-1 bridge, exec-only git, session dispatch | Accepted |
|
| [ADR-006](decisions/006-mapping-bridge-and-dispatch.md) | Mapping decisions: route-1 bridge, exec-only git, session dispatch | Accepted |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
Tracked in [open-questions.md](open-questions.md) — OQ-01 … OQ-13,
|
Tracked in [open-questions.md](open-questions.md), OQ-01 … OQ-13,
|
||||||
seeded from the phase 0 POC findings and research docs. **All thirteen
|
seeded from the phase 0 POC findings and research docs. **All thirteen
|
||||||
are resolved**: the three mapping-detail decisions settled this pass
|
are resolved**: the three mapping-detail decisions settled this pass
|
||||||
(OQ-08 shell convention → assembly-supplied serve-config field, no
|
(OQ-08 shell convention → assembly-supplied serve-config field, no
|
||||||
crate default; OQ-10 → closed as the derivation contract on
|
crate default; OQ-10 → closed as the derivation contract on
|
||||||
`mapping.md`'s exit rows + the upstream PTY-path constraint; OQ-11 →
|
`mapping.md`'s exit rows + the upstream PTY-path constraint; OQ-11 →
|
||||||
universal compile + runtime Unix-gated decline), and the previously
|
universal compile + runtime Unix-gated decline), and the previously
|
||||||
deferred entries closed as fully-decided dispositions (OQ-01/04/09/13
|
deferred entries closed as fully-decided dispositions (OQ-01/04/09/13,
|
||||||
— the decision surfaces were already fixed by ADR-001/ADR-004 and the
|
whose decision surfaces were already fixed by ADR-001/ADR-004 and the
|
||||||
decline postures; what remained was forecasts about consumers, not
|
decline postures; what remained was forecasts about consumers, not
|
||||||
pending alkssh decisions). Upstream dependencies are listed as watch
|
pending alkssh decisions). Upstream dependencies are listed as watch
|
||||||
items in the tracker, not OQs.
|
items in the tracker, not OQs.
|
||||||
@@ -66,12 +66,12 @@ items in the tracker, not OQs.
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- `docs/sdd_process.md` — the SDD process
|
- `docs/sdd_process.md` (the SDD process)
|
||||||
- `docs/research/` — phase 0 (vision, POC plans + findings, russh
|
- `docs/research/`: phase 0 (vision, POC plans + findings, russh
|
||||||
inventory, auth bridge, mapping table, reverse-runner extraction)
|
inventory, auth bridge, mapping table, reverse-runner extraction)
|
||||||
- Family: `/workspace/@alkdev/alkfamily/docs/architecture/decisions/001..003`
|
- Family: `/workspace/@alkdev/alkfamily/docs/architecture/decisions/001..003`
|
||||||
(ADR-003 is the alkssh structural decision)
|
(ADR-003 is the alkssh structural decision)
|
||||||
- Sibling: alkcall ADRs 002/003/004/006/009/010/034/042/051 (ALPN
|
- Sibling: alkcall ADRs 002/003/004/006/009/010/034/042/051 (ALPN
|
||||||
dispatch, auth surface, channels wire/relay); alkhttp ADR-014/073
|
dispatch, auth surface, channels wire/relay); alkhttp ADR-014/073
|
||||||
(secret-material flow and door-surface posture — the patterns
|
(secret-material flow and door-surface posture, the patterns
|
||||||
ADR-005 and `auth.md` mirror)
|
ADR-005 and `auth.md` mirror)
|
||||||
+34
-34
@@ -3,7 +3,7 @@ status: reviewed
|
|||||||
last_updated: 2026-10-28
|
last_updated: 2026-10-28
|
||||||
---
|
---
|
||||||
|
|
||||||
# Auth — identity is the family's, not SSH's
|
# Auth: identity is the family's, not SSH's
|
||||||
|
|
||||||
## The model
|
## The model
|
||||||
|
|
||||||
@@ -20,43 +20,43 @@ verifies a signature; its auth surface is *dispositional* (ADR-004).
|
|||||||
### The bridge
|
### The bridge
|
||||||
|
|
||||||
1. `auth_publickey(user, VerifiedPublicKey)` fires (post-verification;
|
1. `auth_publickey(user, VerifiedPublicKey)` fires (post-verification;
|
||||||
0.65.0 shape — the key is the `.public_key` field, and the client's
|
the 0.65.0 shape has the key as the `.public_key` field, and the
|
||||||
signature `.algorithm` is exposed for optional hardening).
|
client's signature `.algorithm` is exposed for optional hardening).
|
||||||
2. alkssh computes the fingerprint via the `ssh-key` re-export
|
2. alkssh computes the fingerprint via the `ssh-key` re-export
|
||||||
(`HashAlg::Sha256`) — `SHA256:` + unpadded base64 over the
|
(`HashAlg::Sha256`): `SHA256:` + unpadded base64 over the
|
||||||
standard alphabet (`ssh-keygen -lf` byte-format; not the
|
standard alphabet (`ssh-keygen -lf` byte-format; not the
|
||||||
url-safe alphabet). The string format is a **contract invariant**
|
url-safe alphabet). The string format is a **contract invariant**
|
||||||
(ADR-004): parity with assembly-registered values is proven
|
(ADR-004): parity with assembly-registered values is proven
|
||||||
against independent issuers (`ssh-keygen -lf` — POC-1 §1, pinned
|
against independent issuers (`ssh-keygen -lf`; POC-1 §1, pinned
|
||||||
by the committed vectors in `tests/vectors/`).
|
by the committed vectors in `tests/vectors/`).
|
||||||
3. `resolve_from_fingerprint` → `Some(identity)` → accept; `None` →
|
3. `resolve_from_fingerprint` → `Some(identity)` → accept; `None` →
|
||||||
reject — fail-closed, and the resolved-identity-`None` path never
|
reject, fail-closed; the resolved-identity-`None` path never
|
||||||
creates a "weakly authorized" session.
|
creates a "weakly authorized" session.
|
||||||
4. On success the identity rides the session (`auth_succeeded`): for
|
4. On success the identity rides the session (`auth_succeeded`): for
|
||||||
mapped channels it propagates through the alkcall transport
|
mapped channels it propagates through the alkcall transport
|
||||||
(`set_identity`, once) so `AccessControl::check(peer_identity)`
|
(`set_identity`, once) so `AccessControl::check(peer_identity)`
|
||||||
gates every mapped open — the **visible-surface invariant** holds
|
gates every mapped open; the **visible-surface invariant** holds
|
||||||
in the SSH direction with no SSH-shaped exception. POC-3 §C.3
|
in the SSH direction with no SSH-shaped exception. POC-3 §C.3
|
||||||
observed the propagation live (the establisher witness).
|
observed the propagation live (the establisher witness).
|
||||||
|
|
||||||
### Dispositions (ADR-004 — summarized)
|
### Dispositions (ADR-004, summarized)
|
||||||
|
|
||||||
`publickey` is the only implemented method, and the only accepted key
|
`publickey` is the only implemented method, and the only accepted key
|
||||||
algorithm is **ed25519** — `auth_publickey` declines non-ed25519
|
algorithm is **ed25519**, so `auth_publickey` declines non-ed25519
|
||||||
public keys explicitly (the family's ed25519-only posture, ADR-002
|
public keys explicitly (the family's ed25519-only posture, ADR-002
|
||||||
decision 4; one key algorithm carries the family's identity — the
|
decision 4; one key algorithm carries the family's identity: the
|
||||||
same ed25519 keys behind alktls's self-signed certs, iroh, and the
|
same ed25519 keys behind alktls's self-signed certs, iroh, and the
|
||||||
alknet rewrite). `auth_publickey_offered`
|
alknet rewrite). `auth_publickey_offered`
|
||||||
accepts-and-delegates (keeping `max_auth_attempts` honest — the hook
|
accepts-and-delegates (keeping `max_auth_attempts` honest; the hook
|
||||||
does not consume an attempt, POC-1 verified); `auth_none` /
|
does not consume an attempt, POC-1 verified); `auth_none` /
|
||||||
`auth_password` / `auth_keyboard_interactive` / `auth_openssh_certificate`
|
`auth_password` / `auth_keyboard_interactive` / `auth_openssh_certificate`
|
||||||
reject explicitly (at this pin russh's defaults already reject; the
|
reject explicitly (at this pin russh's defaults already reject; the
|
||||||
explicit implementations are the family contract — a pin bump could
|
explicit implementations are the family contract, since a pin bump could
|
||||||
change defaults silently). `Config::methods` advertises `{publickey}`
|
change defaults silently). `Config::methods` advertises `{publickey}`,
|
||||||
— exactly what is implemented. The username is **not identity and
|
exactly what is implemented. The username is **not identity and
|
||||||
carries no semantics** (OQ-03 resolved); log exposure is deployment
|
carries no semantics** (OQ-03 resolved); log exposure is deployment
|
||||||
policy. `max_auth_attempts` defaults `10` (the OpenSSH `none`-probe
|
policy. `max_auth_attempts` defaults `10` (the OpenSSH `none`-probe
|
||||||
consumes one — POC-1 §4).
|
consumes one, POC-1 §4).
|
||||||
|
|
||||||
## Client direction
|
## Client direction
|
||||||
|
|
||||||
@@ -66,37 +66,37 @@ Held keys are bearer-token-class secrets: **vault-encrypted at rest
|
|||||||
(alkvault `EncryptedData`, OpenSSH-PEM inner), decrypted at use,
|
(alkvault `EncryptedData`, OpenSSH-PEM inner), decrypted at use,
|
||||||
zeroized ownership at the adapter seam**; never env vars, never
|
zeroized ownership at the adapter seam**; never env vars, never
|
||||||
plaintext at rest, never persisted after use. The vault is a secret
|
plaintext at rest, never persisted after use. The vault is a secret
|
||||||
store on this path — not a key generator, not a custodian, and never
|
store on this path, not a key generator, not a custodian, and never
|
||||||
involved in resolving *peers'* fingerprints. Failure modes are all
|
involved in resolving *peers'* fingerprints. Failure modes are all
|
||||||
fail-closed with distinct errors: locked / absent / decryption
|
fail-closed with distinct errors: locked / absent / decryption
|
||||||
failure / unknown `key_version` — and rotation preserves key identity.
|
failure / unknown `key_version`, and rotation preserves key identity.
|
||||||
The same model covers **host keys** (decrypt-at-construction into the
|
The same model covers **host keys** (decrypt-at-construction into the
|
||||||
server's `Config::keys`; locked vault → **no server is constructed** —
|
server's `Config::keys`; locked vault → **no server is constructed**;
|
||||||
POC-2 §5).
|
POC-2 §5).
|
||||||
|
|
||||||
### Host-key policy (OQ-05 resolution; the client-side trust contract)
|
### Host-key policy (OQ-05 resolution; the client-side trust contract)
|
||||||
|
|
||||||
Two cases, one seam — the crate is **policy-blind**:
|
Two cases, one seam; the crate is **policy-blind**:
|
||||||
|
|
||||||
1. **Known beforehand** (anchors exist; family-built runners mint
|
1. **Known beforehand** (anchors exist; family-built runners mint
|
||||||
their host key at assembly time): assembly-supplied pins —
|
their host key at assembly time): assembly-supplied pins, with a
|
||||||
**hard-decline on mismatch** at `check_server_key`.
|
**hard decline on mismatch** at `check_server_key`.
|
||||||
2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no anchor
|
2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no anchor
|
||||||
until contact): the crate surfaces
|
until contact): the crate surfaces
|
||||||
(host, port, presented fingerprint) to an **assembly-provided
|
(host, port, presented fingerprint) to an **assembly-provided
|
||||||
`HostKeyPolicy`** — prompt-and-record, provider-metadata match,
|
`HostKeyPolicy`**, which chooses prompt-and-record,
|
||||||
learn-and-record, or accept-and-continue (the ephemeral-fleet
|
provider-metadata match, learn-and-record, or accept-and-continue
|
||||||
shape). The OpenSSH "add to known_hosts?" moment becomes
|
(the ephemeral-fleet shape). The OpenSSH "add to known_hosts?"
|
||||||
assembly policy.
|
moment becomes assembly policy.
|
||||||
|
|
||||||
Crate rules: default policy is fail-closed (reject unknown); the
|
Crate rules: default policy is fail-closed (reject unknown); the
|
||||||
crate never decides trust and never hard-codes a trust model —
|
crate never decides trust and never hard-codes a trust model, so
|
||||||
accept-all is not a built-in crate option, but a permissive case-2
|
accept-all is not a built-in crate option, but a permissive case-2
|
||||||
policy is the assembly's to choose where its fleet model calls for
|
policy is the assembly's to choose where its fleet model calls for
|
||||||
it (alkminer's accept-all for rental fleets is that shape, made
|
it (alkminer's accept-all for rental fleets is that shape, made
|
||||||
deliberately: fresh container per rent → fresh host key, so no pin
|
deliberately: fresh container per rent → fresh host key, so no pin
|
||||||
can exist beforehand; the security anchor there is hub-held
|
can exist beforehand; the security anchor there is hub-held
|
||||||
per-instance dial keys — ADR-005's custody model — plus what rides
|
per-instance dial keys (ADR-005's custody model) plus what rides
|
||||||
the tunnels, not host pins).
|
the tunnels, not host pins).
|
||||||
|
|
||||||
## What is out of scope
|
## What is out of scope
|
||||||
@@ -104,21 +104,21 @@ the tunnels, not host pins).
|
|||||||
- Password / keyboard-interactive anywhere (server: declined; client:
|
- Password / keyboard-interactive anywhere (server: declined; client:
|
||||||
non-goal until a use case).
|
non-goal until a use case).
|
||||||
- OpenSSH certificate auth (`auth_openssh_certificate` declined
|
- OpenSSH certificate auth (`auth_openssh_certificate` declined
|
||||||
unconditionally — OQ-04 resolved: no CA-trust story exists in the
|
unconditionally; OQ-04 resolved: no CA-trust story exists in the
|
||||||
family identity model; future support is new-ADR territory with the
|
family identity model; future support is new-ADR territory with the
|
||||||
upstream identity-model work as its prerequisite).
|
upstream identity-model work as its prerequisite).
|
||||||
- ssh-agent integration/forwarding (external signer processes are out
|
- ssh-agent integration/forwarding (external signer processes are out
|
||||||
of family scope; the vault is the custody story).
|
of family scope; the vault is the custody story).
|
||||||
- TOFU *as crate behavior* — policy lives at the assembly, the
|
- TOFU *as crate behavior*, since policy lives at the assembly: the
|
||||||
crate is policy-blind (above).
|
crate is policy-blind (above).
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
None open or deferred — see [open-questions.md](open-questions.md)
|
None open or deferred; see [open-questions.md](open-questions.md)
|
||||||
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-03 (the
|
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-03 (the
|
||||||
username disposition — §Dispositions), OQ-04 (the certificate decline
|
username disposition, §Dispositions), OQ-04 (the certificate decline,
|
||||||
— §out-of-scope), OQ-05 (the host-key policy seam — §Host-key policy),
|
§out-of-scope), OQ-05 (the host-key policy seam, §Host-key policy),
|
||||||
OQ-06 / OQ-07 (custody boundaries — ADR-005).
|
OQ-06 / OQ-07 (custody boundaries, ADR-005).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
@@ -128,6 +128,6 @@ OQ-06 / OQ-07 (custody boundaries — ADR-005).
|
|||||||
refinement), `poc-1-auth-bridge.md` (§1–4), `poc-2-vault-keys.md`
|
refinement), `poc-1-auth-bridge.md` (§1–4), `poc-2-vault-keys.md`
|
||||||
(§2–5), `reverse-runner.md` (lessons 5–6)
|
(§2–5), `reverse-runner.md` (lessons 5–6)
|
||||||
- alkcall ADRs 002/003/006 (identity surface) and 010 (secret-material
|
- alkcall ADRs 002/003/006 (identity surface) and 010 (secret-material
|
||||||
flow — alkhttp ADR-014's alkcall analog), alkhttp ADR-073 (one
|
flow, alkhttp ADR-014's alkcall analog), alkhttp ADR-073 (one
|
||||||
backend behind every door surface), family ADR-002 decision 5 /
|
backend behind every door surface), family ADR-002 decision 5 /
|
||||||
family ADR-003 decision 2
|
family ADR-003 decision 2
|
||||||
+57
-55
@@ -3,14 +3,14 @@ status: reviewed
|
|||||||
last_updated: 2026-10-29
|
last_updated: 2026-10-29
|
||||||
---
|
---
|
||||||
|
|
||||||
# Client — client direction: driving vanilla SSH servers as family ops
|
# Client (client direction): driving vanilla SSH servers as family ops
|
||||||
|
|
||||||
## The door framing
|
## The door framing
|
||||||
|
|
||||||
The client direction is a door *to vanilla servers*: family in,
|
The client direction is a door *to vanilla servers*: family in,
|
||||||
vanilla out. The motivating consumer is the **reverse-runner** shape
|
vanilla out. The motivating consumer is the **reverse-runner** shape
|
||||||
(`docs/research/reverse-runner.md`, alkminer extraction): a family hub
|
(`docs/research/reverse-runner.md`, alkminer extraction): a family hub
|
||||||
dials vanilla `sshd` on rented/near-ephemeral hosts — remote boxes
|
dials vanilla `sshd` on rented/near-ephemeral hosts; the remote boxes
|
||||||
expose plain sshd and hold nothing of ours; every op the hub drives
|
expose plain sshd and hold nothing of ours; every op the hub drives
|
||||||
maps family-side onto the same protocols the server direction serves.
|
maps family-side onto the same protocols the server direction serves.
|
||||||
The asymmetry is honest (vision principle 2): the server direction
|
The asymmetry is honest (vision principle 2): the server direction
|
||||||
@@ -24,76 +24,77 @@ up into the family surface.
|
|||||||
|
|
||||||
## The dial surface
|
## The dial surface
|
||||||
|
|
||||||
Every dial needs: address, **key source** (ADR-005 — vault-backed
|
Every dial needs: address, **key source** (ADR-005, a vault-backed
|
||||||
blob, decrypt-at-dial, fail-closed on locked/absent — never env vars,
|
blob: decrypt-at-dial, fail-closed on locked/absent, never env vars,
|
||||||
never plaintext at rest), **host-key policy** (OQ-05 resolution —
|
never plaintext at rest), **host-key policy** (the OQ-05 resolution,
|
||||||
assembly-provided; crate default fail-closed reject-unknown, hard
|
assembly-provided; crate default fail-closed reject-unknown, hard
|
||||||
decline on mismatch; the OpenSSH "add to known_hosts?" moment is an
|
decline on mismatch; the OpenSSH "add to known_hosts?" moment is an
|
||||||
assembly policy, e.g. learn-and-record / prompt / provider-metadata
|
assembly policy, e.g. learn-and-record / prompt / provider-metadata
|
||||||
match — the crate is policy-blind), and the bounded-resource config
|
match, and the crate is policy-blind), and the bounded-resource config
|
||||||
(`keepalive_interval`/`keepalive_max` — 15 s / 3 default posture per
|
(`keepalive_interval`/`keepalive_max`, a 15 s / 3 default posture per
|
||||||
the reverse-runner evidence; `inactivity_timeout`; window/packet
|
the reverse-runner evidence; `inactivity_timeout`; window/packet
|
||||||
knobs per `overview.md`).
|
knobs per `overview.md`).
|
||||||
|
|
||||||
The identity the dial authenticates *as* is a registered fingerprint
|
The identity the dial authenticates *as* is a registered fingerprint
|
||||||
at the remote end — for family-run peers (alkforge runner) that's the
|
at the remote end: for family-run peers (alkforge runner) that's the
|
||||||
alkcall `IdentityProvider`; for vanilla fleets it's whatever the
|
alkcall `IdentityProvider`; for vanilla fleets it's whatever the
|
||||||
vanilla server's `authorized_keys` holds — either way the *key* is
|
vanilla server's `authorized_keys` holds. Either way the *key* is
|
||||||
ours from the vault and the host's key is checked by the policy.
|
ours from the vault and the host's key is checked by the policy.
|
||||||
|
|
||||||
## Op rows (family op → vanilla translation)
|
## Op rows (family op → vanilla translation)
|
||||||
|
|
||||||
| Family-facing op | Family protocol/types | Vanilla-wire translation | Feature |
|
| Family-facing op | Family protocol/types | Vanilla-wire translation | Feature |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| run a command / own a shell on the runner | alktty session types (pipe vs PTY; exit semantics) | `channel_open_session` + `exec`/pty-req/shell-req; data pump; drain `wait()` per the POC-3 §D.3 mechanics (`Channel::exec()` — no `request_exec`; Success/Failure arrive on the channel queue, not as `Err` from the request call) | `session` |
|
| run a command / own a shell on the runner | alktty session types (pipe vs PTY; exit semantics) | `channel_open_session` + `exec`/pty-req/shell-req; data pump; drain `wait()` per the POC-3 §D.3 mechanics (`Channel::exec()`, not `request_exec`; Success/Failure arrive on the channel queue, not as `Err` from the request call) | `session` |
|
||||||
| open a tunnel target | alktunnels `TunnelSession`/target types | `channel_open_direct_tcpip` per connection — the typed open resolves to `Err(ChannelOpenFailure)` on server reject (POC-1 §6); no `wait()` before `into_stream` | `tunnel` |
|
| open a tunnel target | alktunnels `TunnelSession`/target types | `channel_open_direct_tcpip` per connection; the typed open resolves to `Err(ChannelOpenFailure)` on server reject (POC-1 §6); no `wait()` before `into_stream` | `tunnel` |
|
||||||
| the `-D` behavior (local SOCKS door) | alksocks' local door **composed at the assembly**, fed a `DialFn` backed by row 2's open | the wire sees only `direct-tcpip`; alkssh *does not depend on alksocks* on this path (ADR-003 §1a) | `tunnel` |
|
| the `-D` behavior (local SOCKS door) | alksocks' local door **composed at the assembly**, fed a `DialFn` backed by row 2's open | the wire sees only `direct-tcpip`; alkssh *does not depend on alksocks* on this path (ADR-003 §1a) | `tunnel` |
|
||||||
| listen + ingest (`-R` leg) | alktunnels consumer/`ForwardedHandler` shape | `tcpip_forward(address, port) -> bound` global request (no mpsc sink param) + **accept the server-initiated `forwarded-tcpip` opens** via `server_channel_open_forwarded_tcpip`, routing each to the family handler | `tunnel` |
|
| listen + ingest (`-R` leg) | alktunnels consumer/`ForwardedHandler` shape | `tcpip_forward(address, port) -> bound` global request (no mpsc sink param) + **accept the server-initiated `forwarded-tcpip` opens** via `server_channel_open_forwarded_tcpip`, routing each to the family handler | `tunnel` |
|
||||||
| upstream git consumption | alkgit client/service types | the same exec grammar the server side parses (`git-(upload,receive)-pack '/repo'` exec), pointed at an upstream `sshd` | `git` |
|
| upstream git consumption | alkgit client/service types | the same exec grammar the server side parses (`git-(upload,receive)-pack '/repo'` exec), pointed at an upstream `sshd` | `git` |
|
||||||
| artifact/deploy transfer | alkfs client half (future) | `subsystem "sftp"` + the alkfs protocol | `sftp` (decline placeholder — OQ-13 resolved) |
|
| artifact/deploy transfer | alkfs client half (future) | `subsystem "sftp"` + the alkfs protocol | `sftp` (decline placeholder, OQ-13 resolved) |
|
||||||
|
|
||||||
## The acceptor: fail-closed client-side
|
## The acceptor: fail-closed client-side
|
||||||
|
|
||||||
russh's client handler receives server-initiated channel opens; its
|
russh's client handler receives server-initiated channel opens; its
|
||||||
default posture for a forwarded open is **accept-and-drop** — a
|
default posture for a forwarded open is **accept-and-drop**, a
|
||||||
half-capability, which the family forbids (ADR-003 symmetry +
|
half-capability, which the family forbids (ADR-003 symmetry +
|
||||||
`reverse-runner.md` lesson 1). The client contract:
|
`reverse-runner.md` lesson 1). The client contract:
|
||||||
|
|
||||||
- Server-initiated opens are served only when the assembly registered
|
- Server-initiated opens are served only when the assembly registered
|
||||||
an acceptor **for that type and under its feature** (`forwarded-tcpip`
|
an acceptor **for that type and under its feature** (the real case
|
||||||
under `tunnel` is the real case — the `-R` data path).
|
is `forwarded-tcpip` under `tunnel`, the `-R` data path).
|
||||||
- Unregistered/unfeatured accepted opens are **declined cleanly**,
|
- Unregistered/unfeatured accepted opens are **declined cleanly**,
|
||||||
never silently dropped after accept.
|
never silently dropped after accept.
|
||||||
- Acceptor handlers may be installed across a live connection's
|
- Acceptor handlers may be installed across a live connection's
|
||||||
lifetime (the alkminer `install_forward_sink` late-install dynamic —
|
lifetime (the alkminer `install_forward_sink` late-install dynamic:
|
||||||
consumers turn capabilities on/off per connection; the shape is an
|
consumers turn capabilities on/off per connection; the shape is an
|
||||||
assembly-mutable acceptor registry).
|
assembly-mutable acceptor registry).
|
||||||
|
|
||||||
> **The landed acceptor shape (`w4-reverse-listen` — recorded at
|
> **The landed acceptor shape (`w4-reverse-listen`, recorded at
|
||||||
> landing):** the registry is **per-type fixed slots** — exactly the
|
> landing):** the registry is **per-type fixed slots**, exactly the
|
||||||
> two `-R` data-leg types the client direction can serve
|
> two `-R` data-leg types the client direction can serve
|
||||||
> (`forwarded-tcpip`, `forwarded-streamlocal`, both under `tunnel`;
|
> (`forwarded-tcpip`, `forwarded-streamlocal`, both under `tunnel`;
|
||||||
> no map, no generic key), one acceptor trait per type; install is
|
> no map, no generic key), one acceptor trait per type; install is
|
||||||
> **refuse-never-replace** (the `ListenRouter::register` recorded
|
> **refuse-never-replace** (the `ListenRouter::register` recorded
|
||||||
> posture, mirrored client-side); the slots are read **at arrival
|
> posture, mirrored client-side); the slots are read **at arrival
|
||||||
> time** by the dial handler's hooks — a pre-install/unfeatured open
|
> time** by the dial handler's hooks, so a pre-install/unfeatured open
|
||||||
> is declined at arrival, never buffered. The payload an accepted
|
> is declined at arrival, never buffered. The payload an accepted
|
||||||
> open hands the assembly: the channel duplex (family-typed — the
|
> open hands the assembly: the channel duplex (family-typed, the
|
||||||
> `-L` rows' `TunnelChannel` byte duplex, one type both legs ride) +
|
> `-L` rows' `TunnelChannel` byte duplex, one type both legs ride) +
|
||||||
> the open's echo/originator fields; **the pump is the acceptor's**
|
> the open's echo/originator fields; **the pump is the acceptor's**
|
||||||
> from the handoff on (the family handler shape) — the crate keeps no
|
> from the handoff on (the family handler shape), and the crate keeps no
|
||||||
> accepted-open ledger. Bounded bookkeeping, the cap relation: the
|
> accepted-open ledger. Bounded bookkeeping, the cap relation: the
|
||||||
> registry bounded by construction (two slots); the
|
> registry bounded by construction (two slots); the
|
||||||
> **reverse-listen count** and the **accepted-open handoff count**
|
> **reverse-listen count** and the **accepted-open handoff count**
|
||||||
> are two dedicated counters, each against the same
|
> are two dedicated counters, each against the same
|
||||||
> `Limits::max_concurrent_channels` ceiling (the server-side
|
> `Limits::max_concurrent_channels` ceiling (the server-side
|
||||||
> listener/ListenerCap posture mirrored — independent counts, one
|
> listener/ListenerCap posture mirrored: independent counts, one
|
||||||
> ceiling; the listens live at the peer, the counter exists so a dial
|
> ceiling; the listens live at the peer, and the counter exists so a dial
|
||||||
> cannot mint an unbounded listener set). The **forward-request rows
|
> cannot mint an unbounded listener set). The **forward-request rows
|
||||||
> gate on the registered acceptor** (the issuing-side disposition:
|
> gate on the registered acceptor** (the issuing-side disposition:
|
||||||
> a listen whose data leg cannot serve is never asked for —
|
> a listen whose data leg cannot serve is never asked for;
|
||||||
> install-first, `OpError::NoAcceptor` pre-wire), plus
|
> install-first, `OpError::NoAcceptor` pre-wire), plus
|
||||||
> `OpError::ListenRefused` (the peer's `RequestDenied` — denied
|
> `OpError::ListenRefused` (the peer's `RequestDenied`:
|
||||||
|
> denied
|
||||||
> listens and unknown-listener cancels, sshd raw-string parity) and
|
> listens and unknown-listener cancels, sshd raw-string parity) and
|
||||||
> `OpError::ListenCapExceeded` (the ceiling, pre-wire). The
|
> `OpError::ListenCapExceeded` (the ceiling, pre-wire). The
|
||||||
> **runtime-Unix gate** rides the unix *listen* rows only (the bind
|
> **runtime-Unix gate** rides the unix *listen* rows only (the bind
|
||||||
@@ -108,28 +109,28 @@ half-capability, which the family forbids (ADR-003 symmetry +
|
|||||||
`tcpip_forward -> u32`); every listener/handle is explicitly
|
`tcpip_forward -> u32`); every listener/handle is explicitly
|
||||||
cancellable; disconnects are explicit (`Disconnect::ByApplication`).
|
cancellable; disconnects are explicit (`Disconnect::ByApplication`).
|
||||||
- **No reconnect or restore policy is owned by the crate.**
|
- **No reconnect or restore policy is owned by the crate.**
|
||||||
Reconnect-and-restore (rebinding persisted tunnel ports — the
|
Reconnect-and-restore (rebinding persisted tunnel ports, the
|
||||||
alkminer flow) is assembly policy; the crate provides the
|
alkminer flow) is assembly policy; the crate provides the
|
||||||
primitives (bound ports, handles, cancellation, clean close) to
|
primitives (bound ports, handles, cancellation, clean close) to
|
||||||
make that possible.
|
make that possible.
|
||||||
|
|
||||||
> **The landed lifecycle surface (`w4-lifecycle` — recorded at
|
> **The landed lifecycle surface (`w4-lifecycle`, recorded at
|
||||||
> landing):** the `-R` listens return **`ReverseListener` handles** —
|
> landing):** the `-R` listens return **`ReverseListener` handles**,
|
||||||
> the `ReverseEndpoint` carried (the *actually bound* values:
|
> with the `ReverseEndpoint` carried (the *actually bound* values:
|
||||||
> the wire address + the bound port / the socket path — the
|
> the wire address + the bound port / the socket path, the
|
||||||
> assembly's own record for a rebind policy it may compose), the
|
> assembly's own record for a rebind policy it may compose) and the
|
||||||
> explicit `cancel()` the wire-honest teardown (the confirmed cancel
|
> explicit `cancel()` as the wire-honest teardown (the confirmed cancel
|
||||||
> also releases the connection's listener count; a repeat rides the
|
> also releases the connection's listener count; a repeat rides the
|
||||||
> wire and hits the unknown-listener refusal). The **stateless**
|
> wire and hits the unknown-listener refusal). The **stateless**
|
||||||
> cancel forms (`reverse_listen_cancel` + the unix twin) ride beside
|
> cancel forms (`reverse_listen_cancel` + the unix twin) ride beside
|
||||||
> the handles 1:1 (sshd's global-request cancels are stateless — a
|
> the handles 1:1 (sshd's global-request cancels are stateless, so a
|
||||||
> cancel by the raw wire strings works from anywhere, including
|
> cancel by the raw wire strings works from anywhere, including
|
||||||
> after the handle itself is gone: the handle shares the
|
> after the handle itself is gone: the handle shares the
|
||||||
> connection's internals through the arc, so its `cancel()` is the
|
> connection's internals through the arc, so its `cancel()` is the
|
||||||
> same wire op). **No `Drop` hides a wire op** on any of these
|
> same wire op). **No `Drop` hides a wire op** on any of these
|
||||||
> handles: a cancellation/bundle of the wire ops is async and cannot
|
> handles: a cancellation/bundle of the wire ops is async and cannot
|
||||||
> run in a sync `Drop`, and at this russh pin dropping the last
|
> run in a sync `Drop`, and at this russh pin dropping the last
|
||||||
> wire-stack handle ends the client runner — the transport *breaks*
|
> wire-stack handle ends the client runner, so the transport *breaks*
|
||||||
> (the peer observes an error-class session end, never the clean
|
> (the peer observes an error-class session end, never the clean
|
||||||
> disconnect message). The primitives-not-policy posture is the
|
> disconnect message). The primitives-not-policy posture is the
|
||||||
> module-docs contract: `client.rs` §Lifecycle posture (the
|
> module-docs contract: `client.rs` §Lifecycle posture (the
|
||||||
@@ -140,36 +141,36 @@ half-capability, which the family forbids (ADR-003 symmetry +
|
|||||||
|
|
||||||
## What the crate does *not* include (client direction)
|
## What the crate does *not* include (client direction)
|
||||||
|
|
||||||
- **Job supervision** — background-job lifecycle (the alkminer
|
- **Job supervision**: background-job lifecycle (the alkminer
|
||||||
start/stop/logs job model, pid files, host-side ledgers) is
|
start/stop/logs job model, pid files, host-side ledgers) is
|
||||||
assembly work composed over the exec rows; the crate gives the
|
assembly work composed over the exec rows; the crate gives the
|
||||||
channels and the exit semantics (including the pipe-mode
|
channels and the exit semantics (including the pipe-mode
|
||||||
pid-only signal caveat — `mapping.md`'s derivation contract).
|
pid-only signal caveat from `mapping.md`'s derivation contract).
|
||||||
- **Credential storage policy** — assembly (the vault handle is
|
- **Credential storage policy**: assembly (the vault handle is
|
||||||
injected; ADR-005 / OQ-06).
|
injected; ADR-005 / OQ-06).
|
||||||
- **Trust decisions** — the host-key policy is assembly-provided
|
- **Trust decisions**: the host-key policy is assembly-provided
|
||||||
(OQ-05; the crate default rejects; a permissive case-2 policy for
|
(OQ-05; the crate default rejects; a permissive case-2 policy for
|
||||||
ephemeral fleets is the assembly's to choose — the crate carries no
|
ephemeral fleets is the assembly's to choose, since the crate
|
||||||
trust model of its own).
|
carries no trust model of its own).
|
||||||
- **sftp** — the `sftp` feature is the tested decline placeholder on
|
- **sftp**: the `sftp` feature is the tested decline placeholder on
|
||||||
both directions (OQ-13 resolved); the alkfs-protocol mapping
|
both directions (OQ-13 resolved); the alkfs-protocol mapping
|
||||||
arrives with alkfs, as its own ADR.
|
arrives with alkfs, as its own ADR.
|
||||||
|
|
||||||
## Correctness facts to carry (POC-recorded)
|
## Correctness facts to carry (POC-recorded)
|
||||||
|
|
||||||
> **The landed op-row shape (`w4-op-rows` — recorded at landing):**
|
> **The landed op-row shape (`w4-op-rows`, recorded at landing):**
|
||||||
> the exit-*consuming* rows (session exec/pty, git exec) ride the
|
> the exit-*consuming* rows (session exec/pty, git exec) ride the
|
||||||
> **split-halves wait-pump** (`Channel::split()` + a spawned task
|
> **split-halves wait-pump** (`Channel::split()` + a spawned task
|
||||||
> draining `wait()`), never `into_stream()` — the conversion's reader
|
> draining `wait()`), never `into_stream()`: the conversion's reader
|
||||||
> side swallows exactly the non-`Data` messages the exit reports ride
|
> side swallows exactly the non-`Data` messages the exit reports ride
|
||||||
> on, so a wait-pump is the only shape that can consume them (the
|
> on, so a wait-pump is the only shape that can consume them (the
|
||||||
> same reason `w4-git-exit-close` pinned the split-halves shape
|
> same reason `w4-git-exit-close` pinned the split-halves shape
|
||||||
> server-side). Only the exit-less rows (the tunnel directs) use
|
> server-side). Only the exit-less rows (the tunnel directs) use
|
||||||
> `into_stream()`, straight after the typed open — no queue
|
> `into_stream()`, straight after the typed open; no queue
|
||||||
> consumption ever precedes a conversion, which is POC-1 §6's caveat
|
> consumption ever precedes a conversion, which is POC-1 §6's caveat
|
||||||
> respected by construction. The session rows' family plane is the
|
> respected by construction. The session rows' family plane is the
|
||||||
> alktty chunk framing both directions (stdin/`CTRL_IN` out, stdout/
|
> alktty chunk framing both directions (stdin/`CTRL_IN` out, stdout/
|
||||||
> stderr in, the family exit chunk in-band, last — `exit_code.rs`'s
|
> stderr in, the family exit chunk in-band, last, per `exit_code.rs`'s
|
||||||
> two-arm contract: the server arm derives wire reports from family
|
> two-arm contract: the server arm derives wire reports from family
|
||||||
> codes, the client arm mirrors wire reports into family codes,
|
> codes, the client arm mirrors wire reports into family codes,
|
||||||
> `−signum` through the one table, the `−1` sentinel never widened).
|
> `−signum` through the one table, the `−1` sentinel never widened).
|
||||||
@@ -180,28 +181,29 @@ half-capability, which the family forbids (ADR-003 symmetry +
|
|||||||
|
|
||||||
- `Channel::exec()` (no `request_exec` at this pin); pty/subsystem
|
- `Channel::exec()` (no `request_exec` at this pin); pty/subsystem
|
||||||
request failures surface as `ChannelMsg::Failure` via `wait()`, not
|
request failures surface as `ChannelMsg::Failure` via `wait()`, not
|
||||||
`Err` from the call — the caller must drain until the expected
|
`Err` from the call; the caller must drain until the expected
|
||||||
message, `continue` past the rest.
|
message and `continue` past the rest.
|
||||||
- `window_change`/`signal` are fire-and-forget and must be sent
|
- `window_change`/`signal` are fire-and-forget and must be sent
|
||||||
**before** `into_stream()` (the Channel is consumed by conversion);
|
**before** `into_stream()` (the Channel is consumed by conversion);
|
||||||
server-side they carry no SSH reply.
|
server-side they carry no SSH reply.
|
||||||
- Do not consume the channel queue before `into_stream()` — a
|
- Do not consume the channel queue before `into_stream()`: a
|
||||||
`wait()` on a fresh channel breaks it (POC-1 §6 caveat), both sides.
|
`wait()` on a fresh channel breaks it (POC-1 §6 caveat), both sides.
|
||||||
- Keepalives on cloud fleets: 15 s / 3 (OpenSSH
|
- Keepalives on cloud fleets: 15 s / 3 (OpenSSH
|
||||||
`ServerAliveInterval=15`/`CountMax=3` parity) — with russh's
|
`ServerAliveInterval=15`/`CountMax=3` parity), since with russh's
|
||||||
default (none), dead NAT'd connections sit unnoticed until an exec
|
default (none) dead NAT'd connections sit unnoticed until an exec
|
||||||
health check fails (`reverse-runner.md` lesson 3).
|
health check fails (`reverse-runner.md` lesson 3).
|
||||||
- A rejected channel open is an `Err` from the typed open call (not a
|
- A rejected channel open is an `Err` from the typed open call (not a
|
||||||
`wait()`-match); the connection stays usable after declines.
|
`wait()`-match); the connection stays usable after declines.
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
None open or deferred — see [open-questions.md](open-questions.md)
|
None open or deferred; see [open-questions.md](open-questions.md)
|
||||||
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-05 (the
|
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-05 (the
|
||||||
host-key policy — §The dial surface), OQ-12 (lifecycle primitives only
|
host-key policy, §The dial surface), OQ-12 (lifecycle primitives only,
|
||||||
— §Lifecycle primitives), OQ-13 (the sftp decline placeholder — §op
|
§Lifecycle primitives), OQ-13 (the sftp decline placeholder, §op
|
||||||
rows and §not-include), OQ-07 (the adapter-side `Zeroizing` wrap —
|
rows and §not-include), OQ-07 (closed upstream: the vault returns a
|
||||||
ADR-005 decision 3).
|
`Zeroizing<String>` from alkvault 0.2.0; ADR-005 decision 3 amended,
|
||||||
|
the adapter-side wrap dropped).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ status: reviewed
|
|||||||
last_updated: 2026-10-28
|
last_updated: 2026-10-28
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-001: The `alk/ssh` ALPN pin — edge-termination contract for `SshAdapter`
|
# ADR-001: The `alk/ssh` ALPN pin (edge-termination contract for `SshAdapter`)
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**Accepted** — 2026-10-26. One-way door (ledger rule 1: ALPN registry
|
**Accepted** (2026-10-26). One-way door (ledger rule 1: ALPN registry
|
||||||
entries are stable — "once published, does not change"). The first
|
entries are stable; "once published, does not change"). The first
|
||||||
consumer is alkforge (family ADR-003 decision 4: the pin lands before
|
consumer is alkforge (family ADR-003 decision 4: the pin lands before
|
||||||
alkforge becomes the consumer).
|
alkforge becomes the consumer).
|
||||||
|
|
||||||
@@ -21,11 +21,11 @@ pin means:
|
|||||||
- **ADR-004** (ALPN convention + connection model) reserves
|
- **ADR-004** (ALPN convention + connection model) reserves
|
||||||
`alk/ssh` → `SshAdapter` as a custom protocol.
|
`alk/ssh` → `SshAdapter` as a custom protocol.
|
||||||
- **ADR-034** (channels wire format) grants `alk/ssh` stream_types
|
- **ADR-034** (channels wire format) grants `alk/ssh` stream_types
|
||||||
`[0, 1]` — "SSH multiplexes internally, including its own control".
|
`[0, 1]` ("SSH multiplexes internally, including its own control").
|
||||||
The channels layer is byte-blind to SSH; it must never parse SSH
|
The channels layer is byte-blind to SSH; it must never parse SSH
|
||||||
channel data.
|
channel data.
|
||||||
- **ADR-042 / ADR-051** (hub relay): "The hub never runs a handler for
|
- **ADR-042 / ADR-051** (hub relay): "The hub never runs a handler for
|
||||||
`alk/tty`, `alk/ssh`, or `alk/tunnel`" — SSH channels are relayed
|
`alk/tty`, `alk/ssh`, or `alk/tunnel`"; SSH channels are relayed
|
||||||
payload-blind between spokes (translate + byte-forward with id
|
payload-blind between spokes (translate + byte-forward with id
|
||||||
rewrite, ACL fired at `channel/open` by the hub's `CallAdapter`).
|
rewrite, ACL fired at `channel/open` by the hub's `CallAdapter`).
|
||||||
- **ADR-009** (BiStream handler leaf) sketches the browser route: a
|
- **ADR-009** (BiStream handler leaf) sketches the browser route: a
|
||||||
@@ -36,12 +36,12 @@ pin means:
|
|||||||
Phase 0 left the topology open (`mapping-table.md` §topology; vision
|
Phase 0 left the topology open (`mapping-table.md` §topology; vision
|
||||||
scope question 1): whether `alk/ssh` carries raw vanilla-SSH bytes
|
scope question 1): whether `alk/ssh` carries raw vanilla-SSH bytes
|
||||||
between alkssh instances (a relay leg), serves the browser route, or
|
between alkssh instances (a relay leg), serves the browser route, or
|
||||||
both — and how that coexists with ADR-042's relay ruling. POC-3
|
both, and how that coexists with ADR-042's relay ruling. POC-3
|
||||||
settled what could be settled by evidence: the served topology is
|
settled what could be settled by evidence: the served topology is
|
||||||
**edge termination** (russh terminates vanilla SSH at the alkssh
|
**edge termination** (russh terminates vanilla SSH at the alkssh
|
||||||
endpoint; family seams are consumed in-process or via `alk/channels`
|
endpoint; family seams are consumed in-process or via `alk/channels`
|
||||||
bridge legs), worked end to end with vanilla clients through published
|
bridge legs), worked end to end with vanilla clients through published
|
||||||
sibling crates — and no leg of it requires the hub to parse SSH.
|
sibling crates, and no leg of it requires the hub to parse SSH.
|
||||||
|
|
||||||
The decision that remains is the *pin*: what contract `alk/ssh`
|
The decision that remains is the *pin*: what contract `alk/ssh`
|
||||||
commits to on the ledger, so the ALPN string cannot be reinterpreted
|
commits to on the ledger, so the ALPN string cannot be reinterpreted
|
||||||
@@ -52,10 +52,10 @@ later.
|
|||||||
1. **The pin.** `alk/ssh` is pinned for exactly one contract: a
|
1. **The pin.** `alk/ssh` is pinned for exactly one contract: a
|
||||||
connection opened under `alk/ssh` carries a **raw vanilla-SSH byte
|
connection opened under `alk/ssh` carries a **raw vanilla-SSH byte
|
||||||
stream**, unchanged and uninterpreted, and the receiving endpoint
|
stream**, unchanged and uninterpreted, and the receiving endpoint
|
||||||
is `SshAdapter::handle(Connection)` — alkssh's russh server stack
|
is `SshAdapter::handle(Connection)`, alkssh's russh server stack
|
||||||
mounted at an alkcall `channel/open` boundary. Each bidi stream is
|
mounted at an alkcall `channel/open` boundary. Each bidi stream is
|
||||||
one independent SSH transport with its own KEX, auth, and channel
|
one independent SSH transport with its own KEX, auth, and channel
|
||||||
multiplexing (hence ADR-034's `[0, 1]` — SSH owns its own
|
multiplexing (hence ADR-034's `[0, 1]`: SSH owns its own
|
||||||
multiplexing, and the channels layer stays byte-blind, per
|
multiplexing, and the channels layer stays byte-blind, per
|
||||||
ADR-042's relay ruling).
|
ADR-042's relay ruling).
|
||||||
2. **Payload-blind relay is part of the contract.** Any intermediate
|
2. **Payload-blind relay is part of the contract.** Any intermediate
|
||||||
@@ -64,8 +64,8 @@ later.
|
|||||||
`channel/open` and never an SSH handler. This is ADR-042's ruling,
|
`channel/open` and never an SSH handler. This is ADR-042's ruling,
|
||||||
restated as the ALPN's own contract so it cannot drift.
|
restated as the ALPN's own contract so it cannot drift.
|
||||||
3. **Phase 1 serves edge termination only.** The first deployment
|
3. **Phase 1 serves edge termination only.** The first deployment
|
||||||
shape — russh terminating vanilla SSH at the alkssh edge, family
|
shape (russh terminating vanilla SSH at the alkssh edge, family
|
||||||
seams consumed in-process — does not speak `alk/ssh` itself: it is
|
seams consumed in-process) does not speak `alk/ssh` itself: it is
|
||||||
a door, not a relay party. No pin behavior in this ADR requires the
|
a door, not a relay party. No pin behavior in this ADR requires the
|
||||||
relay leg to exist.
|
relay leg to exist.
|
||||||
4. **The relay leg and the browser-WASM route remain compatible by
|
4. **The relay leg and the browser-WASM route remain compatible by
|
||||||
@@ -77,7 +77,7 @@ later.
|
|||||||
surface; nothing in the crate is gated on a consumer).
|
surface; nothing in the crate is gated on a consumer).
|
||||||
5. **What the pin is not.** It is not a carrier for alkcall's framed
|
5. **What the pin is not.** It is not a carrier for alkcall's framed
|
||||||
channels traffic (that has its own protocol); it is not a second
|
channels traffic (that has its own protocol); it is not a second
|
||||||
SSH-flavored control channel; and honesty cuts both ways — an
|
SSH-flavored control channel; and honesty cuts both ways: an
|
||||||
endpoint that declines to serve `alk/ssh` must refuse the open,
|
endpoint that declines to serve `alk/ssh` must refuse the open,
|
||||||
never accept-and-stall (the same honest-advertisement rule that
|
never accept-and-stall (the same honest-advertisement rule that
|
||||||
runs through every alkssh decision).
|
runs through every alkssh decision).
|
||||||
@@ -88,18 +88,18 @@ later.
|
|||||||
(frozen here) from the *served edge topology* (which evolves with
|
(frozen here) from the *served edge topology* (which evolves with
|
||||||
the family seams); the browser and relay routes are pre-enabled at
|
the family seams); the browser and relay routes are pre-enabled at
|
||||||
zero pin cost; the ledger gets a single unambiguous entry.
|
zero pin cost; the ledger gets a single unambiguous entry.
|
||||||
- **Negative**: one-way door — the raw-vanilla-SSH-stream contract
|
- **Negative**: the pin is a one-way door; the raw-vanilla-SSH-stream
|
||||||
binds every future consumer (alkforge first). If a consumer ever
|
contract binds every future consumer (alkforge first). If a consumer ever
|
||||||
needed framed or pre-parsed SSH over `alk/ssh`, that would be a new
|
needed framed or pre-parsed SSH over `alk/ssh`, that would be a new
|
||||||
ALPN + new ADR, not a reinterpretation.
|
ALPN + new ADR, not a reinterpretation.
|
||||||
- **Neutral**: `SshAdapter::handle` mounts the full russh server
|
- **Neutral**: `SshAdapter::handle` mounts the full russh server
|
||||||
stack per stream, so endpoints pay per-connection bounded-resource
|
stack per stream, so endpoints pay per-connection bounded-resource
|
||||||
limits (window, auth-attempt, inactivity ceilings) — the alkssh
|
limits (window, auth-attempt, inactivity ceilings); the alkssh
|
||||||
limits contract applies per `alk/ssh` stream, not per transport.
|
limits contract applies per `alk/ssh` stream, not per transport.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Family ADR-003 (`003-alkssh-russh-adapter.md`) — decisions 1 and 4
|
- Family ADR-003 (`003-alkssh-russh-adapter.md`), decisions 1 and 4
|
||||||
- alkcall ADR-002 (ProtocolHandler ALPN claim), ADR-004
|
- alkcall ADR-002 (ProtocolHandler ALPN claim), ADR-004
|
||||||
(the `alk/ssh` → `SshAdapter` reservation), ADR-009 (browser-WASM
|
(the `alk/ssh` → `SshAdapter` reservation), ADR-009 (browser-WASM
|
||||||
sketch), ADR-034 (`alk/ssh` stream_types `[0, 1]`), ADR-042/051
|
sketch), ADR-034 (`alk/ssh` stream_types `[0, 1]`), ADR-042/051
|
||||||
@@ -107,5 +107,5 @@ later.
|
|||||||
- `docs/research/mapping-table.md` §topology,
|
- `docs/research/mapping-table.md` §topology,
|
||||||
`docs/research/vision.md` scope Q1,
|
`docs/research/vision.md` scope Q1,
|
||||||
`docs/research/poc-3-mapping-bakeoff.md` (edge termination, proven)
|
`docs/research/poc-3-mapping-bakeoff.md` (edge termination, proven)
|
||||||
- OQ-01 (resolved — no consumer in scope; this ADR is the entire
|
- OQ-01 (resolved: no consumer in scope; this ADR is the entire
|
||||||
decision surface)
|
decision surface)
|
||||||
@@ -3,16 +3,16 @@ status: reviewed
|
|||||||
last_updated: 2026-10-28
|
last_updated: 2026-10-28
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-002: The russh pin — 0.65.0, aws-lc-rs, no compression, ed25519-only
|
# ADR-002: The russh pin (0.65.0, aws-lc-rs, no compression, ed25519-only)
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**Accepted** — 2026-10-26. A one-way door in practice (SSH negotiation
|
**Accepted** (2026-10-26). A one-way door in practice (SSH negotiation
|
||||||
behavior crosses crate boundaries; bumps are deliberate, tested, and
|
behavior crosses crate boundaries; bumps are deliberate, tested, and
|
||||||
recorded per the repo's wire-stability convention). Amended same day
|
recorded per the repo's wire-stability convention). Amended same day
|
||||||
after review: the earlier enabling of the `rsa` feature is reverted
|
after review: the earlier enabling of the `rsa` feature is reverted
|
||||||
(`rsa` off). Amended 2026-10-27 (review pass): the key-algorithm
|
(`rsa` off). Amended 2026-10-27 (review pass): the key-algorithm
|
||||||
position is **ed25519-only** — the family's posture is the binding
|
position is **ed25519-only**; the family's posture is the binding
|
||||||
constraint, not fleet compatibility (decision 4).
|
constraint, not fleet compatibility (decision 4).
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
@@ -21,9 +21,9 @@ constraint, not fleet compatibility (decision 4).
|
|||||||
2026-10-26 re-verification section carries the current facts. The POCs
|
2026-10-26 re-verification section carries the current facts. The POCs
|
||||||
validated against 0.64.1 (then-newest) with the `ring` backend; since
|
validated against 0.64.1 (then-newest) with the `ring` backend; since
|
||||||
then **0.65.0** has been published (2026-10-25) with six security
|
then **0.65.0** has been published (2026-10-25) with six security
|
||||||
fixes — three in classes an internet-facing SSH door cares about
|
fixes, three of them in classes an internet-facing SSH door cares
|
||||||
(client EXT_INFO unbounded allocation, peer zero-max-packet-size
|
about (client EXT_INFO unbounded allocation, peer zero-max-packet-size
|
||||||
channel stall, pre-auth nested-certificate stack exhaustion) — and two
|
channel stall, pre-auth nested-certificate stack exhaustion), and two
|
||||||
breaking changes confined to handler-signature shapes
|
breaking changes confined to handler-signature shapes
|
||||||
(`PublicKeyOffer` / `VerifiedPublicKey`; `async-trait` feature
|
(`PublicKeyOffer` / `VerifiedPublicKey`; `async-trait` feature
|
||||||
removed).
|
removed).
|
||||||
@@ -31,9 +31,9 @@ removed).
|
|||||||
The family's crypto-posture context: `alktls`, `alkhttp`, and
|
The family's crypto-posture context: `alktls`, `alkhttp`, and
|
||||||
`alknet` all standardize on `rustls` with the **`aws_lc_rs`** feature;
|
`alknet` all standardize on `rustls` with the **`aws_lc_rs`** feature;
|
||||||
no family crate links `ring`. The provider choice matters only as
|
no family crate links `ring`. The provider choice matters only as
|
||||||
binary-composition hygiene — when alkforge's binary links alkssh
|
binary-composition hygiene (when alkforge's binary links alkssh
|
||||||
*and* the family's rustls stack, one shared primitive library is the
|
*and* the family's rustls stack, the goal is one shared primitive
|
||||||
goal — and has nothing to do with TLS on the SSH wire.
|
library) and has nothing to do with TLS on the SSH wire.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
@@ -51,22 +51,22 @@ goal — and has nothing to do with TLS on the SSH wire.
|
|||||||
builds clean in ~50 s with no external provisioning (prebuilt
|
builds clean in ~50 s with no external provisioning (prebuilt
|
||||||
bindings), tree `aws-lc-rs 1.18.2` / `aws-lc-sys 0.46.0`. The
|
bindings), tree `aws-lc-rs 1.18.2` / `aws-lc-sys 0.46.0`. The
|
||||||
POCs' `ring` build is recorded as the fallback if a target
|
POCs' `ring` build is recorded as the fallback if a target
|
||||||
platform ever fails to provision aws-lc — a deliberate,
|
platform ever fails to provision aws-lc, a deliberate,
|
||||||
recorded deviation, never drift.
|
recorded deviation, never drift.
|
||||||
3. **`flate2` off — no compression.** The POCs already ran
|
3. **No compression (`flate2` off).** The POCs already ran
|
||||||
compression-free and vanilla OpenSSH was untroubled; SSH
|
compression-free and vanilla OpenSSH was untroubled; SSH
|
||||||
compression is largely pointless at modern link layers and is the
|
compression is largely pointless at modern link layers and is the
|
||||||
CRIME-adjacent attack surface OpenSSH itself defaults off.
|
CRIME-adjacent attack surface OpenSSH itself defaults off.
|
||||||
Bounded-resources and surface-reduction posture agree.
|
Bounded-resources and surface-reduction posture agree.
|
||||||
4. **ed25519-only — `rsa` off, and no other scheme is supported.**
|
4. **ed25519-only (`rsa` off, and no other scheme is supported).**
|
||||||
Amended 2026-10-27 from the earlier "ed25519-first" wording after
|
Amended 2026-10-27 from the earlier "ed25519-first" wording after
|
||||||
review: the position is ed25519-only. One key algorithm carries
|
review: the position is ed25519-only. One key algorithm carries
|
||||||
the family's identity: the same ed25519 keys serve alktls's
|
the family's identity: the same ed25519 keys serve alktls's
|
||||||
self-signed TLS certificates and iroh, and the downstream alknet
|
self-signed TLS certificates and iroh, and the downstream alknet
|
||||||
rewrite rides ssh + ed25519, tcp + tls, quic + tls, and iroh — the
|
rewrite rides ssh + ed25519, tcp + tls, quic + tls, and iroh, the
|
||||||
same ed25519 identity. The legacy/alternative SSH key schemes
|
same ed25519 identity. The legacy/alternative SSH key schemes
|
||||||
(rsa, dsa; ecdsa variants and hardware-token sk variants share the
|
(rsa, dsa; ecdsa variants and hardware-token sk variants share the
|
||||||
defect) are issued, stored, or derived nowhere in the family —
|
defect) are issued, stored, or derived nowhere in the family, so
|
||||||
supporting them here would be a wart backed by nothing else.
|
supporting them here would be a wart backed by nothing else.
|
||||||
Enabling `rsa` would additionally have pulled two RC-pinned deps
|
Enabling `rsa` would additionally have pulled two RC-pinned deps
|
||||||
and made alkssh the family's only RSA-holding crate. Concretely:
|
and made alkssh the family's only RSA-holding crate. Concretely:
|
||||||
@@ -75,15 +75,15 @@ goal — and has nothing to do with TLS on the SSH wire.
|
|||||||
RC-free except `ssh-key =0.7.0-rc.11`, which russh exact-pins on
|
RC-free except `ssh-key =0.7.0-rc.11`, which russh exact-pins on
|
||||||
the `russh::keys` path). Wire behavior is honest and fail-closed:
|
the `russh::keys` path). Wire behavior is honest and fail-closed:
|
||||||
a client presenting a non-ed25519 key gets a clean
|
a client presenting a non-ed25519 key gets a clean
|
||||||
`USERAUTH_FAILURE` — never a half-open session. Operational
|
`USERAUTH_FAILURE`, never a half-open session. Operational
|
||||||
shape: alkssh implements no other scheme; an assembly registers
|
shape: alkssh implements no other scheme; an assembly registers
|
||||||
ed25519 fingerprints only; and `auth_publickey` **declines
|
ed25519 fingerprints only; and `auth_publickey` **declines
|
||||||
non-ed25519 keys explicitly** (ADR-004's explicit-over-inherited
|
non-ed25519 keys explicitly** (ADR-004's explicit-over-inherited
|
||||||
pattern applied to the algorithm position, so the posture survives
|
pattern applied to the algorithm position, so the posture survives
|
||||||
a pin bump that might otherwise accept more schemes). If a scheme
|
a pin bump that might otherwise accept more schemes). If a scheme
|
||||||
ever matters, the amendment is one feature flag plus this ADR.
|
ever matters, the amendment is one feature flag plus this ADR.
|
||||||
5. **`des` / `dsa` never.** russh's own "Danger: insecure" features —
|
5. **`des` / `dsa` never.** russh's own "Danger: insecure" features,
|
||||||
no alkssh configuration may enable them.
|
which no alkssh configuration may enable.
|
||||||
6. **Manifest lands with the dependency**: `rust-version = "1.89"`
|
6. **Manifest lands with the dependency**: `rust-version = "1.89"`
|
||||||
(russh's MSRV; supersedes the current 1.88) and, with it, the
|
(russh's MSRV; supersedes the current 1.88) and, with it, the
|
||||||
edition-2024 bump the POCs compiled under.
|
edition-2024 bump the POCs compiled under.
|
||||||
@@ -91,12 +91,12 @@ goal — and has nothing to do with TLS on the SSH wire.
|
|||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **Positive**: the pin is current, carries the security fixes, and
|
- **Positive**: the pin is current, carries the security fixes, and
|
||||||
matches the family crypto posture; the tree is fully inventoried —
|
matches the family crypto posture; the tree is fully inventoried,
|
||||||
with `rsa` off it is **RC-free except `ssh-key =0.7.0-rc.11`**
|
and with `rsa` off it is **RC-free except `ssh-key =0.7.0-rc.11`**
|
||||||
(unavoidable: russh exact-pins it on the `russh::keys` path);
|
(unavoidable: russh exact-pins it on the `russh::keys` path);
|
||||||
MSRV/licensing are explicit.
|
MSRV/licensing are explicit.
|
||||||
- **Negative**: clients presenting non-ed25519 keys (RSA among them)
|
- **Negative**: clients presenting non-ed25519 keys (RSA among them)
|
||||||
decline outright (clean `USERAUTH_FAILURE`) — recorded as
|
decline outright (clean `USERAUTH_FAILURE`), recorded as
|
||||||
intentional; `ssh-key`'s RC pin rides the russh keys path until
|
intentional; `ssh-key`'s RC pin rides the russh keys path until
|
||||||
russh moves off it; aws-lc-rs is the heavier build (proven
|
russh moves off it; aws-lc-rs is the heavier build (proven
|
||||||
acceptable here, with `ring` as the recorded fallback).
|
acceptable here, with `ring` as the recorded fallback).
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ status: reviewed
|
|||||||
last_updated: 2026-10-28
|
last_updated: 2026-10-28
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-003: The capability feature graph — flat, independent, two-direction
|
# ADR-003: The capability feature graph (flat, independent, two-direction)
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**Accepted** — 2026-10-26. Builds the fail-closed capability contract
|
**Accepted** (2026-10-26). Builds the fail-closed capability contract
|
||||||
(family ADR-003 decision 3) into the crate's own feature names; the
|
(family ADR-003 decision 3) into the crate's own feature names; the
|
||||||
decline paths are contract territory on every side of every feature.
|
decline paths are contract territory on every side of every feature.
|
||||||
|
|
||||||
@@ -17,15 +17,15 @@ decline paths are contract territory on every side of every feature.
|
|||||||
into capabilities by what rides it (shell/exec, git, pipes), and
|
into capabilities by what rides it (shell/exec, git, pipes), and
|
||||||
vanilla SSH multiplexes tunnel legs through `direct-tcpip`/`forwarded-tcpip`
|
vanilla SSH multiplexes tunnel legs through `direct-tcpip`/`forwarded-tcpip`
|
||||||
channels (a vanilla `ssh -D` is its own client-side SOCKS door plus
|
channels (a vanilla `ssh -D` is its own client-side SOCKS door plus
|
||||||
`direct-tcpip` — the wire never sees SOCKS, POC-3 correction 2). The feature
|
`direct-tcpip`; the wire never sees SOCKS, POC-3 correction 2). The feature
|
||||||
graph is how alkforge gets an endpoint that is *structurally incapable*
|
graph is how alkforge gets an endpoint that is *structurally incapable*
|
||||||
of serving what it didn't enable: a git-only build accepts no shell and
|
of serving what it didn't enable: a git-only build accepts no shell and
|
||||||
no tunnels — and, in the opposite direction, no capabilities a client
|
no tunnels; and, in the opposite direction, no capabilities a client
|
||||||
build didn't opt into.
|
build didn't opt into.
|
||||||
|
|
||||||
Phase 0 validated the seams end to end (POC-3 stages A–D: git exec,
|
Phase 0 validated the seams end to end (POC-3 stages A–D: git exec,
|
||||||
direct-tcpip via the alktunnels bridge, session/pty, all against
|
direct-tcpip via the alktunnels bridge, session/pty, all against
|
||||||
published sibling crates — with stage B additionally establishing,
|
published sibling crates, with stage B additionally establishing,
|
||||||
against real vanilla `ssh -D` + curl, that the wire-level SOCKS story
|
against real vanilla `ssh -D` + curl, that the wire-level SOCKS story
|
||||||
is client-side; its SOCKS-in-channel serve path was subsequently
|
is client-side; its SOCKS-in-channel serve path was subsequently
|
||||||
retired, see §1a), and the user's direction confirmed the scope: full
|
retired, see §1a), and the user's direction confirmed the scope: full
|
||||||
@@ -40,53 +40,52 @@ feature set except sftp (placeholder until alkfs lands).
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `session` | session channels: pty/shell, pipe-mode exec | client pty/shell/exec/subsystem opens | alktty |
|
| `session` | session channels: pty/shell, pipe-mode exec | client pty/shell/exec/subsystem opens | alktty |
|
||||||
| `git` | git-parseable exec (dispatched out of session channels) | client git consumption of upstream servers | alkgit |
|
| `git` | git-parseable exec (dispatched out of session channels) | client git consumption of upstream servers | alkgit |
|
||||||
| `tunnel` | `direct-tcpip`, `forwarded-tcpip`, `direct-streamlocal`, the `tcpip_forward`/`streamlocal_forward` listen flow (the streamlocal rows — channel and listen — are runtime-Unix-gated, OQ-11) | client `direct-tcpip` opens (which is what `-D` rides — see 1a) | alktunnels |
|
| `tunnel` | `direct-tcpip`, `forwarded-tcpip`, `direct-streamlocal`, the `tcpip_forward`/`streamlocal_forward` listen flow (the streamlocal rows, channel and listen, are runtime-Unix-gated, OQ-11) | client `direct-tcpip` opens (which is what `-D` rides; see 1a) | alktunnels |
|
||||||
| `sftp` | *placeholder — declines until alkfs lands (OQ-13 resolved)* | client sftp half, same placeholder | alkfs (future) |
|
| `sftp` | *placeholder, declines until alkfs lands (OQ-13 resolved)* | client sftp half, same placeholder | alkfs (future) |
|
||||||
|
|
||||||
**1a. No `dynamic-forward` feature.** Amended 2026-10-26 (review
|
**1a. No `dynamic-forward` feature.** Amended 2026-10-26 (review
|
||||||
untangle): the earlier graph carried a `dynamic-forward` feature
|
untangle): the earlier graph carried a `dynamic-forward` feature
|
||||||
with a server-side SOCKS5-in-channel row — a family-native client
|
with a server-side SOCKS5-in-channel row, a family-native client
|
||||||
shape ("speak SOCKS into a session channel, server dials through
|
shape ("speak SOCKS into a session channel, server dials through
|
||||||
alksocks"). No consumer of ours is that client: vanilla `ssh -D`
|
alksocks"). No consumer of ours is that client: vanilla `ssh -D`
|
||||||
does its SOCKS5 handshake client-side and opens plain
|
does its SOCKS5 handshake client-side and opens plain
|
||||||
`direct-tcpip` channels (POC-3 correction 2), and the family
|
`direct-tcpip` channels (POC-3 correction 2), and the family
|
||||||
behavior of `-D` is a **client-side local door** — alksocks' local
|
behavior of `-D` is a **client-side local door**: alksocks' local
|
||||||
door given a `DialFn` backed by the alkssh client's
|
door given a `DialFn` backed by the alkssh client's
|
||||||
`direct-tcpip` opens, composed at the assembly. The server needs
|
`direct-tcpip` opens, composed at the assembly. The server needs
|
||||||
only the `tunnel` row for the whole `-D` leg, and alkssh does not
|
only the `tunnel` row for the whole `-D` leg, and alkssh does not
|
||||||
depend on alksocks on this path. The SSH-detect dispatch from
|
depend on alksocks on this path. The SSH-detect dispatch from
|
||||||
ADR-006 point 3 retires with it (session dispatch is parse-only;
|
ADR-006 point 3 retires with it (session dispatch is parse-only;
|
||||||
a request-less session channel declines). If a consumer ever
|
a request-less session channel declines). If a consumer ever
|
||||||
genuinely wants a remote SOCKS door (the OQ-01 relay/browser leg —
|
genuinely wants a remote SOCKS door (the OQ-01 relay/browser leg,
|
||||||
resolved out of scope: no such consumer exists today), adding it
|
resolved out of scope: no such consumer exists today), adding it
|
||||||
back is an explicit ADR —
|
back is an explicit ADR. The two transports are "just byte
|
||||||
the two transports are "just byte streams", so nothing forbids
|
streams", so nothing forbids it; nothing needs it.
|
||||||
it; nothing needs it.
|
|
||||||
|
|
||||||
2. **Flat and independent.** No feature implies another. `default = []`
|
2. **Flat and independent.** No feature implies another. `default = []`:
|
||||||
— a default build has no capabilities at all; every unfeatured
|
a default build has no capabilities at all; every unfeatured
|
||||||
channel type/open/request declines with a clean error. `git` does
|
channel type/open/request declines with a clean error. `git` does
|
||||||
not imply `session` (a git-only build serves the git-parseable exec
|
not imply `session` (a git-only build serves the git-parseable exec
|
||||||
path by declining to parse to anything else — the parse-then-fail
|
path by declining to parse to anything else, the parse-then-fail
|
||||||
contract from POC-3 stage A). Note the `tunnel` client leg's SOCKS
|
contract from POC-3 stage A). Note the `tunnel` client leg's SOCKS
|
||||||
door is *local to the dialing host* (alksocks composed at the
|
door is *local to the dialing host* (alksocks composed at the
|
||||||
assembly), not a wire capability — the wire sees `direct-tcpip`
|
assembly), not a wire capability; the wire sees `direct-tcpip`
|
||||||
only.
|
only.
|
||||||
3. **Two-direction symmetry.** Each feature name gates *both* the
|
3. **Two-direction symmetry.** Each feature name gates *both* the
|
||||||
server-direction (what an embedded alkssh serves vanilla clients)
|
server-direction (what an embedded alkssh serves vanilla clients)
|
||||||
and the client-direction (what a family app drives when dialing an
|
and the client-direction (what a family app drives when dialing an
|
||||||
upstream SSH server, including the forwarded-channel acceptor rows).
|
upstream SSH server, including the forwarded-channel acceptor rows).
|
||||||
One name, one capability, both doors — this is the symmetry rule,
|
One name, one capability, both doors: this is the symmetry rule,
|
||||||
now structural: a `git`-only build cannot dial a tunnel upstream
|
now structural: a `git`-only build cannot dial a tunnel upstream
|
||||||
any more than it can serve one.
|
any more than it can serve one.
|
||||||
4. **Client-side fail-closed at the acceptor.** Server-initiated
|
4. **Client-side fail-closed at the acceptor.** Server-initiated
|
||||||
channel opens (`forwarded-tcpip` is the real one — the `-R` data
|
channel opens (the real one is `forwarded-tcpip`, the `-R` data
|
||||||
path) are served only under a registered feature's acceptor
|
path) are served only under a registered feature's acceptor
|
||||||
handler; unhandled/unfeatured accepted opens are **declined**, not
|
handler; unhandled/unfeatured accepted opens are **declined**, not
|
||||||
accept-and-dropped (russh's client default is accept-then-drop —
|
accept-and-dropped (russh's client default is accept-then-drop,
|
||||||
ours to fix; the reverse-runner extraction names it).
|
ours to fix; the reverse-runner extraction names it).
|
||||||
5. **Never features.** `x11` and `auth-agent` forwarding are not
|
5. **Never features.** `x11` and `auth-agent` forwarding are not
|
||||||
feature-gated at all — always declined, at any configuration
|
feature-gated at all and are always declined, at any configuration
|
||||||
(agent forwarding is the *anti-pattern* of the alkvault custody
|
(agent forwarding is the *anti-pattern* of the alkvault custody
|
||||||
model; x11 has no family seam). An ADR amendment is required to
|
model; x11 has no family seam). An ADR amendment is required to
|
||||||
ever feature-gate them.
|
ever feature-gate them.
|
||||||
@@ -104,21 +103,21 @@ feature set except sftp (placeholder until alkfs lands).
|
|||||||
surfaces can't drift apart on capability naming.
|
surfaces can't drift apart on capability naming.
|
||||||
- **Negative**: the split of `session` makes feature interplay a
|
- **Negative**: the split of `session` makes feature interplay a
|
||||||
real test matrix (per-feature decline paths on both directions are
|
real test matrix (per-feature decline paths on both directions are
|
||||||
contract tests — the AGENTS.md verification convention covers this
|
contract tests; the AGENTS.md verification convention covers this
|
||||||
via `--all-features` runs plus the decline-matrix suite).
|
via `--all-features` runs plus the decline-matrix suite).
|
||||||
- **Neutral**: feature *names* are now contractual (downstream builds
|
- **Neutral**: feature *names* are now contractual (downstream builds
|
||||||
specify them); renames would be semver-visible — hence the ADR, not
|
specify them); renames would be semver-visible, hence the ADR rather
|
||||||
incidental naming.
|
than incidental naming.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- `mapping-table.md` (the validated rows under these names + the
|
- `mapping-table.md` (the validated rows under these names + the
|
||||||
feature-split notes + the three POC-3 corrections)
|
feature-split notes + the three POC-3 corrections)
|
||||||
- Family ADR-003 (decision 3 — the fail-closed capability features),
|
- Family ADR-003 (decision 3, the fail-closed capability features),
|
||||||
family ADR-002 (why exclusion matters for alkforge)
|
family ADR-002 (why exclusion matters for alkforge)
|
||||||
- `docs/research/reverse-runner.md` (client-direction surface the
|
- `docs/research/reverse-runner.md` (client-direction surface the
|
||||||
symmetry rule serves; the acceptor default fact)
|
symmetry rule serves; the acceptor default fact)
|
||||||
- `docs/research/poc-3-mapping-bakeoff.md` (stages A–D: the seams at
|
- `docs/research/poc-3-mapping-bakeoff.md` (stages A–D: the seams at
|
||||||
these names work end to end)
|
these names work end to end)
|
||||||
- OQ-13 (resolved — the placeholder contract stands), OQ-02 (resolved
|
- OQ-13 (resolved; the placeholder contract stands), OQ-02 (resolved
|
||||||
by ADR-006)
|
by ADR-006)
|
||||||
@@ -3,16 +3,16 @@ status: reviewed
|
|||||||
last_updated: 2026-10-28
|
last_updated: 2026-10-28
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-004: Auth dispositions — publickey-only, fail-closed, fingerprint identity
|
# ADR-004: Auth dispositions (publickey-only, fail-closed, fingerprint identity)
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**Accepted** — 2026-10-26. Evidence-backed (POC-1 end to end,
|
**Accepted** (2026-10-26). Evidence-backed (POC-1 end to end,
|
||||||
POC-2's signature-pipeline facts); open to amendment only via a new
|
POC-2's signature-pipeline facts); open to amendment only via a new
|
||||||
ADR. Amended 2026-10-27 (review pass, per ADR-002 decision 4's
|
ADR. Amended 2026-10-27 (review pass, per ADR-002 decision 4's
|
||||||
ed25519-only amendment): `auth_publickey` additionally declines
|
ed25519-only amendment): `auth_publickey` additionally declines
|
||||||
non-ed25519 keys explicitly — the key-algorithm posture, not the
|
non-ed25519 keys explicitly (the key-algorithm posture, not the
|
||||||
fingerprint disposition, changes; all other dispositions stand. Not a
|
fingerprint disposition, changes); all other dispositions stand. Not a
|
||||||
ledger item (no wire format changes), but the decline
|
ledger item (no wire format changes), but the decline
|
||||||
matrix is contract territory: every decline is tested on both sides of
|
matrix is contract territory: every decline is tested on both sides of
|
||||||
every feature.
|
every feature.
|
||||||
@@ -22,7 +22,7 @@ every feature.
|
|||||||
The family identity model has exactly one SSH-shaped resolution path:
|
The family identity model has exactly one SSH-shaped resolution path:
|
||||||
a verified key's fingerprint → alkcall `IdentityProvider::
|
a verified key's fingerprint → alkcall `IdentityProvider::
|
||||||
resolve_from_fingerprint` → `Identity` (`auth-bridge.md`). russh 0.64.1
|
resolve_from_fingerprint` → `Identity` (`auth-bridge.md`). russh 0.64.1
|
||||||
(the POC pin) performs the cryptographic verification itself —
|
(the POC pin) performs the cryptographic verification itself, so
|
||||||
`auth_publickey` fires *after* signature and ownership verification
|
`auth_publickey` fires *after* signature and ownership verification
|
||||||
(POC-1 observed), and invalid/malformed signatures produce
|
(POC-1 observed), and invalid/malformed signatures produce
|
||||||
`USERAUTH_FAILURE` before any handler hook fires (POC-2); the facts
|
`USERAUTH_FAILURE` before any handler hook fires (POC-2); the facts
|
||||||
@@ -35,25 +35,25 @@ twice: which methods exist, and what unauthenticated attempts receive.
|
|||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
1. **`auth_publickey` is implemented — the only method.** Handler:
|
1. **`auth_publickey` is implemented, and it is the only method.** Handler:
|
||||||
fingerprint (`ssh-key` re-export, `HashAlg::Sha256`,
|
fingerprint (`ssh-key` re-export, `HashAlg::Sha256`,
|
||||||
`SHA256:` + unpadded base64 over the standard alphabet —
|
`SHA256:` + unpadded base64 over the standard alphabet,
|
||||||
`+/`, not the url-safe alphabet; byte-identical to
|
`+/`, not the url-safe alphabet; byte-identical to
|
||||||
`ssh-keygen -lf`) →
|
`ssh-keygen -lf`) →
|
||||||
`resolve_from_fingerprint` → `Some` → accept with the resolved
|
`resolve_from_fingerprint` → `Some` → accept with the resolved
|
||||||
identity; `None` → reject. The fingerprint *string format* is a
|
identity; `None` → reject. The fingerprint *string format* is a
|
||||||
contract invariant: registration strings from any standard issuer
|
contract invariant: registration strings from any standard issuer
|
||||||
(ssh-keygen, alkcall-side computation) must compare equal —
|
(ssh-keygen, alkcall-side computation) must compare equal; POC-1
|
||||||
POC-1 proved parity against `ssh-keygen -lf` output.
|
proved parity against `ssh-keygen -lf` output.
|
||||||
2. **`auth_publickey_offered` is accepted-and-delegated.** The probe
|
2. **`auth_publickey_offered` is accepted-and-delegated.** The probe
|
||||||
hook (key seen, ownership unverified) accepts the offer and lets
|
hook (key seen, ownership unverified) accepts the offer and lets
|
||||||
`auth_publickey` decide — which keeps `max_auth_attempts` counting
|
`auth_publickey` decide, which keeps `max_auth_attempts` counting
|
||||||
honest (confirmed: the hook does not consume an attempt).
|
honest (confirmed: the hook does not consume an attempt).
|
||||||
3. **Every other method is explicitly declined, including when inert
|
3. **Every other method is explicitly declined, including when inert
|
||||||
at the pin.** `auth_none`, `auth_password`,
|
at the pin.** `auth_none`, `auth_password`,
|
||||||
`auth_keyboard_interactive`, `auth_openssh_certificate` each get
|
`auth_keyboard_interactive`, `auth_openssh_certificate` each get
|
||||||
explicit reject implementations. At russh 0.64.1 the defaults
|
explicit reject implementations. At russh 0.64.1 the defaults
|
||||||
already reject (POC-1's correction of the 0.5x-era assumption) —
|
already reject (POC-1's correction of the 0.5x-era assumption);
|
||||||
the explicit implementations are family contract, not redundancy:
|
the explicit implementations are family contract, not redundancy:
|
||||||
the pin is a one-way door and a future bump could silently change
|
the pin is a one-way door and a future bump could silently change
|
||||||
defaults (an auth-posture change would otherwise be invisible).
|
defaults (an auth-posture change would otherwise be invisible).
|
||||||
@@ -86,7 +86,7 @@ twice: which methods exist, and what unauthenticated attempts receive.
|
|||||||
small and fully testable per build; auth posture survives a russh
|
small and fully testable per build; auth posture survives a russh
|
||||||
bump that changes defaults.
|
bump that changes defaults.
|
||||||
- **Negative**: none material. The explicit-declines cost a few
|
- **Negative**: none material. The explicit-declines cost a few
|
||||||
handler lines per method — deliberately purchased immovability.
|
handler lines per method, deliberately purchased immovability.
|
||||||
- **Neutral**: password/keyboard-interactive exist client-side only as
|
- **Neutral**: password/keyboard-interactive exist client-side only as
|
||||||
non-goals (`vision.md`) until a consuming use case arrives; the
|
non-goals (`vision.md`) until a consuming use case arrives; the
|
||||||
server never gains them without amending this ADR (a wire-visible
|
server never gains them without amending this ADR (a wire-visible
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ status: reviewed
|
|||||||
last_updated: 2026-10-28
|
last_updated: 2026-10-28
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-005: At-rest key custody — vault-wrapped OpenSSH-PEM, decrypt-at-use
|
# ADR-005: At-rest key custody (vault-wrapped OpenSSH-PEM, decrypt-at-use)
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**Accepted** — 2026-10-26. Evidence-backed (POC-2, 39/39, including
|
**Accepted** (2026-10-26). Evidence-backed (POC-2, 39/39, including
|
||||||
the 4-way inner-format bake and the failure matrix); amendable only
|
the 4-way inner-format bake and the failure matrix); amendable only
|
||||||
via a new ADR.
|
via a new ADR.
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ via a new ADR.
|
|||||||
On the client direction alkssh *holds* secret material (dial keys); on
|
On the client direction alkssh *holds* secret material (dial keys); on
|
||||||
the server direction it holds the host key. Family rules in force:
|
the server direction it holds the host key. Family rules in force:
|
||||||
|
|
||||||
- Secrets come from alkvault-backed sources — never env vars, never
|
- Secrets come from alkvault-backed sources, never env vars, never
|
||||||
plaintext at rest (family ADR-002 decision 5; family-wide standing
|
plaintext at rest (family ADR-002 decision 5; family-wide standing
|
||||||
posture).
|
posture).
|
||||||
- The vault is a **secret store on this path, not a key generator or
|
- The vault is a **secret store on this path, not a key generator or
|
||||||
@@ -24,12 +24,13 @@ the server direction it holds the host key. Family rules in force:
|
|||||||
a different feature and is not the SSH-keys path (`auth-bridge.md`
|
a different feature and is not the SSH-keys path (`auth-bridge.md`
|
||||||
§The gap). Held SSH keys are bearer-token-class secrets: opaque
|
§The gap). Held SSH keys are bearer-token-class secrets: opaque
|
||||||
bytes, vault-encrypted at rest, decrypted at use.
|
bytes, vault-encrypted at rest, decrypted at use.
|
||||||
- The vault is **never** involved in resolving *clients'* fingerprints
|
- The vault is **never** involved in resolving *clients'* fingerprints;
|
||||||
— that path is the assembly-layer `IdentityProvider` (scope note in
|
that path is the assembly-layer `IdentityProvider` (scope note in
|
||||||
`pocs.md` §POC-2).
|
`pocs.md` §POC-2).
|
||||||
|
|
||||||
alkvault 0.1.0's `encrypt`/`decrypt` surface is `&str` in, `String`
|
alkvault's `encrypt`/`decrypt` surface is `&str` in, `Zeroizing<String>`
|
||||||
out (`EncryptedData` as the wrapper) — a decisive constraint on any
|
out since 0.2.0 (0.1.0 returned a plain `String`; the `EncryptedData`
|
||||||
|
wrapper unchanged), a decisive constraint on any
|
||||||
binary inner format (POC-2 measured: it would force base64 layers on
|
binary inner format (POC-2 measured: it would force base64 layers on
|
||||||
both ends).
|
both ends).
|
||||||
|
|
||||||
@@ -42,9 +43,9 @@ both ends).
|
|||||||
boundary (`ssh -i` accepted the decrypted bytes directly), and
|
boundary (`ssh -i` accepted the decrypted bytes directly), and
|
||||||
exactly one decode call (`decode_secret_key(plain, None)`) from
|
exactly one decode call (`decode_secret_key(plain, None)`) from
|
||||||
russh. Rejected alternatives, recorded for the trail: ssh-key's
|
russh. Rejected alternatives, recorded for the trail: ssh-key's
|
||||||
own encrypted-key format (double-wrapping antipattern — the
|
own encrypted-key format (double-wrapping antipattern: the
|
||||||
passphrase is itself a stored secret, moving the problem one level
|
passphrase is itself a stored secret, moving the problem one level
|
||||||
down), raw-seed base64 (80 B vs 540 B blobs — legitimate size win,
|
down), raw-seed base64 (80 B vs 540 B blobs, a legitimate size win,
|
||||||
rejected because it couples storage to a fixed key shape; "the
|
rejected because it couples storage to a fixed key shape; "the
|
||||||
bytes of a PEM file" survives every algorithm ssh-key can decode),
|
bytes of a PEM file" survives every algorithm ssh-key can decode),
|
||||||
PKCS#8 PEM (parity, loses the native round-trip, adds nothing).
|
PKCS#8 PEM (parity, loses the native round-trip, adds nothing).
|
||||||
@@ -55,20 +56,28 @@ both ends).
|
|||||||
only at dial time: locking it mid-session keeps live sessions
|
only at dial time: locking it mid-session keeps live sessions
|
||||||
streaming while new dials fail (POC-2 verified); unlock is
|
streaming while new dials fail (POC-2 verified); unlock is
|
||||||
side-effect-free on key identity.
|
side-effect-free on key identity.
|
||||||
3. **Zeroization ownership (the known gap).** ssh-key zeroizes
|
3. **Zeroization ownership (the gap, closed upstream).** ssh-key
|
||||||
ed25519 key bytes on `Drop` (decoded keys, and their clones, clean
|
zeroizes ed25519 key bytes on `Drop` (decoded keys, and their
|
||||||
themselves). The *decrypted plaintext* is the gap: alkvault's
|
clones, clean themselves). The *decrypted plaintext* was the gap:
|
||||||
`decrypt` returns a plain `String`, so from the vault's return to
|
alkvault's `decrypt` once returned a plain `String`, leaving the
|
||||||
the adapter's wrap the plaintext sits in unfenced heap. The
|
plaintext in unfenced heap between the vault's return and the
|
||||||
adapter owns the wrap (proven in POC-2) — **that is the contract on
|
adapter's wrap; the adapter owned the wrap then (proven in POC-2;
|
||||||
this path**; the vault-side improvement (a `Zeroizing<String>`-out
|
**that was the contract on this path**) and the vault-side
|
||||||
API) is an upstream ask filed in alkvault's own tracker, not an
|
improvement (a `Zeroizing<String>`-out API) was an upstream ask
|
||||||
alkssh obligation — when it lands, this ADR's pointer updates as a
|
filed in alkvault's own tracker. The ask shipped in **alkvault
|
||||||
one-line doc touch (alkssh OQ-07 is closed at the
|
0.2.0** (2026-11-04, the tracker's review 001; this ADR is the
|
||||||
adapter-convention level).
|
origin it cites): `decrypt` returns `Zeroizing<String>`, the buffer
|
||||||
|
zeroizes on every path (including the UTF-8 error path), and
|
||||||
|
`rotate` transits zeroized output too. The adapter seam wrap is
|
||||||
|
thereby **dropped** (the alkssh 0.2.0 adoption): `decrypt_and_decode`
|
||||||
|
consumes the vault's zeroized return directly, since the double wrap
|
||||||
|
would be dead posture. Decision 3's ownership now sits entirely
|
||||||
|
with alkvault; the adapter's obligation shrinks to consuming the
|
||||||
|
zeroized value in one pass (the `decode_key_arm` signature keeps
|
||||||
|
riding the `Zeroizing<String>` type as the proof).
|
||||||
4. **Failure modes are all fail-closed, with distinct error shapes.**
|
4. **Failure modes are all fail-closed, with distinct error shapes.**
|
||||||
Locked vault → no dial (and no server: a host key that can't load
|
Locked vault → no dial (and no server: a host key that can't load
|
||||||
prevents server construction — verified at bootstrap). Absent
|
prevents server construction, verified at bootstrap). Absent
|
||||||
blob → no dial. Wrong mnemonic / tampered ciphertext → decryption
|
blob → no dial. Wrong mnemonic / tampered ciphertext → decryption
|
||||||
failure, no dial. Unknown `key_version` → vault's `InvalidPath`
|
failure, no dial. Unknown `key_version` → vault's `InvalidPath`
|
||||||
refuse-before-crypto. Rotation (at-rest version bump, key
|
refuse-before-crypto. Rotation (at-rest version bump, key
|
||||||
@@ -76,25 +85,25 @@ both ends).
|
|||||||
each to a distinct variant; the vault's error taxonomy is the
|
each to a distinct variant; the vault's error taxonomy is the
|
||||||
adapter's to reshape, never to collapse.
|
adapter's to reshape, never to collapse.
|
||||||
5. **`EncryptedData` is opaque to alkssh.** The crate never
|
5. **`EncryptedData` is opaque to alkssh.** The crate never
|
||||||
interprets `key_version` or any record internals — the vault owns
|
interprets `key_version` or any record internals; the vault owns
|
||||||
version semantics. Rotation is a storage-side operation.
|
version semantics. Rotation is a storage-side operation.
|
||||||
6. **Host keys ride the same model.** Server host keys are
|
6. **Host keys ride the same model.** Server host keys are
|
||||||
decrypt-at-construction into `Config::keys` through the same
|
decrypt-at-construction into `Config::keys` through the same
|
||||||
load path; the client's host-*pin* is public config, never vault
|
load path; the client's host-*pin* is public config, never vault
|
||||||
material (OQ-05's two-case policy handles pinning). The two-case
|
material (OQ-05's two-case policy handles pinning). The two-case
|
||||||
host-key policy *seam* is the crate's trust-decision contract —
|
host-key policy *seam* is the crate's trust-decision contract: the
|
||||||
the crate is policy-blind (fail-closed default; a permissive
|
crate is policy-blind (fail-closed default; a permissive
|
||||||
case-2 policy is an assembly choice) — the contract lives in
|
case-2 policy is an assembly choice), and the contract lives in
|
||||||
`auth.md`/`client.md`.
|
`auth.md`/`client.md`.
|
||||||
7. **Plaintext never persists.** No disk spill on this path, ever;
|
7. **Plaintext never persists.** No disk spill on this path, ever;
|
||||||
the vanilla-tool PEM compatibility is for the assembly layer to
|
the vanilla-tool PEM compatibility is for the assembly layer to
|
||||||
exploit if ever (e.g. handing a key to an external agent process —
|
exploit if ever (e.g. handing a key to an external agent process,
|
||||||
itself out of family scope).
|
itself out of family scope).
|
||||||
8. **The `Signer` seam is dismissed, with evidence.** POC-2's verdict:
|
8. **The `Signer` seam is dismissed, with evidence.** POC-2's verdict:
|
||||||
no `impl Signer` exists anywhere in the 0.64.1 tree (the doc
|
no `impl Signer` exists anywhere in the 0.64.1 tree (the doc
|
||||||
comment claiming an SSH-agent implementation is stale); a
|
comment claiming an SSH-agent implementation is stale); a
|
||||||
hand-rolled signer must replicate the splice convention (`to_sign
|
hand-rolled signer must replicate the splice convention (`to_sign
|
||||||
|| string{sig}` — initial omission was silently misparsed); an
|
|| string{sig}`; an initial omission was silently misparsed); an
|
||||||
in-process vault gains nothing over one-time decode (decrypted
|
in-process vault gains nothing over one-time decode (decrypted
|
||||||
bytes exist client-side at *either* dial time or sign time;
|
bytes exist client-side at *either* dial time or sign time;
|
||||||
decrypt-per-sign keeps *more* plaintext copies alive while
|
decrypt-per-sign keeps *more* plaintext copies alive while
|
||||||
@@ -107,10 +116,11 @@ both ends).
|
|||||||
future held material; every failure mode is proven fail-closed; the
|
future held material; every failure mode is proven fail-closed; the
|
||||||
at-rest bytes are plain OpenSSH key files, so alkvault stores and
|
at-rest bytes are plain OpenSSH key files, so alkvault stores and
|
||||||
rotates them with no SSH-specific logic.
|
rotates them with no SSH-specific logic.
|
||||||
- **Negative**: decrypted plaintext has one unfenced window (before
|
- **Negative**: ~~decrypted plaintext has one unfenced window (before
|
||||||
the adapter's wrap) until alkvault changes its API — the gap is
|
the adapter's wrap) until alkvault changes its API~~; the gap is
|
||||||
recorded, owned, and filed with its owner (the alkvault tracker;
|
resolved (alkvault 0.2.0's `Zeroizing<String>` return; the adapter
|
||||||
OQ-07 resolved at the adapter level), not silently absorbed.
|
wrap dropped, OQ-07 amended). No plaintext window remains on this
|
||||||
|
ADR's path.
|
||||||
- **Neutral**: the seed-variant size optimization remains noted as a
|
- **Neutral**: the seed-variant size optimization remains noted as a
|
||||||
legitimate future option if blob size ever matters; adopting it
|
legitimate future option if blob size ever matters; adopting it
|
||||||
would be an ADR amendment (it changes what a stored record is).
|
would be an ADR amendment (it changes what a stored record is).
|
||||||
@@ -122,6 +132,6 @@ both ends).
|
|||||||
lifecycle + zeroization bounds, §3 the failure matrix, §4 the
|
lifecycle + zeroization bounds, §3 the failure matrix, §4 the
|
||||||
Signer dismissal, §5 host keys end to end)
|
Signer dismissal, §5 host keys end to end)
|
||||||
- alkcall ADR-010 + alkhttp ADR-014 (the secret-material-flow family
|
- alkcall ADR-010 + alkhttp ADR-014 (the secret-material-flow family
|
||||||
pattern — ADR-010 is alkhttp ADR-014's alkcall analog)
|
pattern; ADR-010 is alkhttp ADR-014's alkcall analog)
|
||||||
- Family ADR-002 decision 5, family ADR-003 decision 2
|
- Family ADR-002 decision 5, family ADR-003 decision 2
|
||||||
- OQ-05, OQ-06, OQ-07, OQ-13 (all resolved)
|
- OQ-05, OQ-06, OQ-07, OQ-13 (all resolved)
|
||||||
@@ -3,14 +3,14 @@ status: reviewed
|
|||||||
last_updated: 2026-10-28
|
last_updated: 2026-10-28
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-006: Mapping decisions — route-1 bridge, exec-only git, session dispatch
|
# ADR-006: Mapping decisions (route-1 bridge, exec-only git, session dispatch)
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**Accepted** — 2026-10-26. All three decisions are
|
**Accepted** (2026-10-26). All three decisions are
|
||||||
POC-3-validated against published sibling crates; amendments are new
|
POC-3-validated against published sibling crates; amendments are new
|
||||||
ADRs. Amended same day (review untangle): point 3's **SOCKS-detect is
|
ADRs. Amended same day (review untangle): point 3's **SOCKS-detect is
|
||||||
retired** — with the `dynamic-forward` feature removed (ADR-003 §1a),
|
retired**: with the `dynamic-forward` feature removed (ADR-003 §1a),
|
||||||
session dispatch is parse-only and a request-less session channel
|
session dispatch is parse-only and a request-less session channel
|
||||||
simply declines. The parse-vs-anything split was only ever needed if
|
simply declines. The parse-vs-anything split was only ever needed if
|
||||||
a session channel could *be* a SOCKS stream; no consumer is that
|
a session channel could *be* a SOCKS stream; no consumer is that
|
||||||
@@ -39,48 +39,48 @@ alkgit/alksocks/alktunnels/alktty).
|
|||||||
(`channel_open_direct_tcpip` → accept only after the fabric open
|
(`channel_open_direct_tcpip` → accept only after the fabric open
|
||||||
succeeds → `copy bidirectional`). POC-3 stage C proved it end to
|
succeeds → `copy bidirectional`). POC-3 stage C proved it end to
|
||||||
end (8 MiB in ~333 ms; fabric-open failure maps to an explicit
|
end (8 MiB in ~333 ms; fabric-open failure maps to an explicit
|
||||||
SSH `ConnectFailed` reject — accept happens only post-success,
|
SSH `ConnectFailed` reject; accept happens only post-success,
|
||||||
since `accept()` consumes the handle). Registry-side bridging
|
since `accept()` consumes the handle). Registry-side bridging
|
||||||
(route 2: alkssh-shaped `ChannelOperations` in a serving
|
(route 2: alkssh-shaped `ChannelOperations` in a serving
|
||||||
endpoint's registry) is **rejected**: it inverts the door posture
|
endpoint's registry) is **rejected**: it inverts the door posture
|
||||||
by putting adapter work upstream of the door. The alktunnels
|
by putting adapter work upstream of the door. The alktunnels
|
||||||
*absence* note (no serve-over-stream entry) is thereby resolved —
|
*absence* note (no serve-over-stream entry) is thereby resolved,
|
||||||
route 1 never needed one.
|
as route 1 never needed one.
|
||||||
2. **Git over SSH is exec-only; every subsystem request declines.**
|
2. **Git over SSH is exec-only; every subsystem request declines.**
|
||||||
The vanilla git connection model is: git spawns its ssh transport,
|
The vanilla git connection model is: git spawns its ssh transport,
|
||||||
which opens one session channel and sends
|
which opens one session channel and sends
|
||||||
`exec_request "git-{upload,receive}-pack '/repo'"` — the repo path
|
`exec_request "git-{upload,receive}-pack '/repo'"`, so the repo path
|
||||||
rides inside the command string, so a subsystem request (name
|
rides inside the command string, and a subsystem request (name
|
||||||
only, no repo id) can never dispatch. POC-3 proved the parse
|
only, no repo id) can never dispatch. POC-3 proved the parse
|
||||||
grammar (quoted/unquoted paths, service-only → decline,
|
grammar (quoted/unquoted paths, service-only → decline,
|
||||||
non-git → parse-then-fail with no resource alloc) and
|
non-git → parse-then-fail with no resource alloc) and
|
||||||
clone/push/second-push through the published seam with zero
|
clone/push/second-push through the published seam with zero
|
||||||
fixups. Vanilla git behavior loses nothing — vanilla git never
|
fixups. Vanilla git behavior loses nothing: vanilla git never
|
||||||
speaks subsystem for git; the decline only refuses a manual
|
speaks subsystem for git; the decline only refuses a manual
|
||||||
`ssh -s`-style invocation, with a reason.
|
`ssh -s`-style invocation, with a reason.
|
||||||
3. **~~Session-channel dispatch: request-driven parse wins; detect
|
3. **~~Session-channel dispatch: request-driven parse wins; detect
|
||||||
covers the request-less case~~ — amended: parse-only.** The
|
covers the request-less case~~ (amended: parse-only).** The
|
||||||
original decision composed the POC-3 modes as: requests take the
|
original decision composed the POC-3 modes as: requests take the
|
||||||
parse path (git under `git`, pipe/shell under `session`);
|
parse path (git under `git`, pipe/shell under `session`);
|
||||||
request-less session channels ran a SOCKS-detect
|
request-less session channels ran a SOCKS-detect
|
||||||
(`dynamic-forward`: spawn, first-byte `0x05` sniff, 500 ms
|
(`dynamic-forward`: spawn, first-byte `0x05` sniff, 500 ms
|
||||||
bounded window). With `dynamic-forward` removed from the feature
|
bounded window). With `dynamic-forward` removed from the feature
|
||||||
graph (ADR-003 §1a), the detect has no owner: **session dispatch
|
graph (ADR-003 §1a), the detect has no owner: **session dispatch
|
||||||
is parse-only** — requests parse to git (exec) or pipe/shell,
|
is parse-only**: requests parse to git (exec) or pipe/shell,
|
||||||
and a request-less session channel declines as unfeatured. This
|
and a request-less session channel declines as unfeatured. This
|
||||||
still resolves OQ-02 (the composition was the unknown; the
|
still resolves OQ-02 (the composition was the unknown; the
|
||||||
resolution is now the simpler of the two candidates).
|
resolution is now the simpler of the two candidates).
|
||||||
4. **The spawn-and-return rule is a structural constraint, not an
|
4. **The spawn-and-return rule is a structural constraint, not an
|
||||||
implementation detail.** russh 0.64.1-era servers run handler
|
implementation detail.** russh 0.64.1-era servers run handler
|
||||||
callbacks inline inside the session's packet loop — a blocking
|
callbacks inline inside the session's packet loop, so a blocking
|
||||||
handler deadlocks against the peer's follow-up bytes (POC-3
|
handler deadlocks against the peer's follow-up bytes (POC-3
|
||||||
proved by construction: receive-pack pushes bytes immediately
|
proved by construction: receive-pack pushes bytes immediately
|
||||||
after the exec request). Every serving path is
|
after the exec request). Every serving path is
|
||||||
`tokio::spawn` + handler-returns-fast; exit reporting from the
|
`tokio::spawn` + handler-returns-fast; exit reporting from the
|
||||||
spawned task rides the cloned `Session::handle()` —
|
spawned task rides the cloned `Session::handle()`:
|
||||||
`exit-status`(then `close`) before channel close, `exit-signal`
|
`exit-status` (then `close`) before channel close, `exit-signal`
|
||||||
for signal termination (signal-name fidelity: the derivation
|
for signal termination (signal-name fidelity: the derivation
|
||||||
contract on `mapping.md`'s exit rows — OQ-10 resolved).
|
contract on `mapping.md`'s exit rows; OQ-10 resolved).
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
@@ -89,11 +89,11 @@ alkgit/alksocks/alktunnels/alktty).
|
|||||||
alkforge assembly gets an exact wiring recipe (POC-3 §C.4's
|
alkforge assembly gets an exact wiring recipe (POC-3 §C.4's
|
||||||
in-process fabric leg is the template, verbatim).
|
in-process fabric leg is the template, verbatim).
|
||||||
- **Negative**: the SOCKS-detect retirement removes a tunable the
|
- **Negative**: the SOCKS-detect retirement removes a tunable the
|
||||||
POC proved workable — if a family client ever wants a remote SOCKS
|
POC proved workable; if a family client ever wants a remote SOCKS
|
||||||
door, the shape is re-addable via ADR (500 ms bounded window
|
door, the shape is re-addable via ADR (500 ms bounded window
|
||||||
preserved in the reference here).
|
preserved in the reference here).
|
||||||
- **Neutral**: route 2 remains documented in `mapping-table.md` as
|
- **Neutral**: route 2 remains documented in `mapping-table.md` as
|
||||||
the rejected alternative, with its rationale — useful if the
|
the rejected alternative, with its rationale, useful if the
|
||||||
posture is ever revisited (that revisit is an ADR, not a code flip).
|
posture is ever revisited (that revisit is an ADR, not a code flip).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
@@ -103,8 +103,8 @@ alkgit/alksocks/alktunnels/alktty).
|
|||||||
- `mapping-table.md` (the rows + the absence note this ADR resolves)
|
- `mapping-table.md` (the rows + the absence note this ADR resolves)
|
||||||
- ADR-003 (the feature gates these dispatch decisions sit behind)
|
- ADR-003 (the feature gates these dispatch decisions sit behind)
|
||||||
- `docs/research/reverse-runner.md` (the client-direction mirror of
|
- `docs/research/reverse-runner.md` (the client-direction mirror of
|
||||||
leg 1 — the `-L`/`-R` legs the same route-1 bridge serves)
|
leg 1, the `-L`/`-R` legs the same route-1 bridge serves)
|
||||||
- OQ-02 (resolved by this ADR), OQ-10 (resolved — the derivation
|
- OQ-02 (resolved by this ADR), OQ-10 (resolved: the derivation
|
||||||
contract lives on `mapping.md`'s exit rows; the PTY-path
|
contract lives on `mapping.md`'s exit rows, and the PTY-path
|
||||||
signal-identity limitation is recorded upstream, alktty/portable-pty
|
signal-identity limitation is recorded upstream on the
|
||||||
side)
|
alktty/portable-pty side)
|
||||||
@@ -3,7 +3,7 @@ status: reviewed
|
|||||||
last_updated: 2026-10-29
|
last_updated: 2026-10-29
|
||||||
---
|
---
|
||||||
|
|
||||||
# Mapping — server direction: SSH channels × family seams (the spine)
|
# Mapping (server direction): SSH channels × family seams, the spine
|
||||||
|
|
||||||
## The door framing
|
## The door framing
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ family out. Vanilla `ssh`/`git` connect as they would to any sshd;
|
|||||||
every accepted channel/request maps to **exactly one family protocol
|
every accepted channel/request maps to **exactly one family protocol
|
||||||
seam** and **exactly one cargo feature** (ADR-003). Rows below are
|
seam** and **exactly one cargo feature** (ADR-003). Rows below are
|
||||||
POC-3-validated against published sibling crates with real vanilla
|
POC-3-validated against published sibling crates with real vanilla
|
||||||
tooling (OpenSSH 9.6, git 2.43, curl 8.5 — `poc-3-mapping-bakeoff.md`).
|
tooling (OpenSSH 9.6, git 2.43, curl 8.5; see `poc-3-mapping-bakeoff.md`).
|
||||||
|
|
||||||
## The seams
|
## The seams
|
||||||
|
|
||||||
@@ -20,25 +20,25 @@ tooling (OpenSSH 9.6, git 2.43, curl 8.5 — `poc-3-mapping-bakeoff.md`).
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| alktty | `drive_session[_pre_negotiated]` | `(impl AsyncWrite, impl AsyncRead, backends, identity)` |
|
| alktty | `drive_session[_pre_negotiated]` | `(impl AsyncWrite, impl AsyncRead, backends, identity)` |
|
||||||
| alkgit | `serve_duplex_resolved` | `(repo_id, identity, service, duplex, limits, backend traits)` |
|
| alkgit | `serve_duplex_resolved` | `(repo_id, identity, service, duplex, limits, backend traits)` |
|
||||||
| alktunnels | — no serve entry (**absence note resolved**: route 1 never needed one, ADR-006) | consumer/open-op registrations |
|
| alktunnels | no serve entry (**absence note resolved**: route 1 never needed one, ADR-006) | consumer/open-op registrations |
|
||||||
|
|
||||||
russh channels convert to duplex via `into_stream`/`split` — the
|
russh channels convert to duplex via `into_stream`/`split`, the
|
||||||
bridge primitives every mapping uses (POC-1 §7).
|
bridge primitives every mapping uses (POC-1 §7).
|
||||||
|
|
||||||
## Channel-type rows
|
## Channel-type rows
|
||||||
|
|
||||||
| SSH channel type | Vanilla use | Feature | Family seam | Contract notes |
|
| SSH channel type | Vanilla use | Feature | Family seam | Contract notes |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `session` → `pty_request` + `shell_request` | `ssh host` interactive shell | `session` | alktty: `NegotiateRequest` constructed locally → `drive_session_pre_negotiated` | `pty_request` args → `TerminalParamsWire` (term/cols/rows/pixels; SSH modes → the reserved modes JSON); shell argv = the assembly-supplied serve-config shell (no crate default; unset → `shell_request` declines — OQ-08 resolved); the data plane is a **translation bridge**, not a byte pump — alktty is chunk-framed both directions (stdin/stdout/stderr/CTRL chunks): SSH data→stdin chunks; stdout chunks→SSH data; stderr chunks→`extended_data(1)`; exit chunk→`exit-status`/`exit-signal` + close (POC-3 §D.1) |
|
| `session` → `pty_request` + `shell_request` | `ssh host` interactive shell | `session` | alktty: `NegotiateRequest` constructed locally → `drive_session_pre_negotiated` | `pty_request` args → `TerminalParamsWire` (term/cols/rows/pixels; SSH modes → the reserved modes JSON); shell argv = the assembly-supplied serve-config shell (no crate default; unset → `shell_request` declines, OQ-08 resolved); the data plane is a **translation bridge**, not a byte pump: alktty is chunk-framed both directions (stdin/stdout/stderr/CTRL chunks): SSH data→stdin chunks; stdout chunks→SSH data; stderr chunks→`extended_data(1)`; exit chunk→`exit-status`/`exit-signal` + close (POC-3 §D.1) |
|
||||||
| `session` → `exec_request` (non-git) | `ssh host "cmd"` | `session` | alktty pipe mode (`terminal: None`) — separate stderr | the wire's command string → the configured exec wrapper argv with the command appended as final element (`wrapper ++ [cmd]`, OQ-08 — never naively split); wrapper unset → clean decline; stderr rides `extended_data(1)` — only valid without PTY; PTY mode merges (kernel PTY, POC-3 §D.1 proven) |
|
| `session` → `exec_request` (non-git) | `ssh host "cmd"` | `session` | alktty pipe mode (`terminal: None`), which separates stderr | the wire's command string → the configured exec wrapper argv with the command appended as final element (`wrapper ++ [cmd]`, OQ-08; never naively split); wrapper unset → clean decline; stderr rides `extended_data(1)` (only valid without PTY); PTY mode merges (kernel PTY, POC-3 §D.1 proven) |
|
||||||
| `session` → `exec_request` git | `git clone/push ssh://` | `git` | alkgit: parse command → `Service` + repo → `serve_duplex_resolved(repo_id, identity, service, duplex, …)` | **exec-only** (ADR-006 decision 2): the repo rides inside the exec command string (`git-upload-pack '/repo'`); vanilla git never speaks subsystem for git; process-mode dialect (server-speaks-first advertisement); exit-status proven both directions |
|
| `session` → `exec_request` git | `git clone/push ssh://` | `git` | alkgit: parse command → `Service` + repo → `serve_duplex_resolved(repo_id, identity, service, duplex, …)` | **exec-only** (ADR-006 decision 2): the repo rides inside the exec command string (`git-upload-pack '/repo'`); vanilla git never speaks subsystem for git; process-mode dialect (server-speaks-first advertisement); exit-status proven both directions |
|
||||||
| `session` → `subsystem_request` | `sftp` clients; manual `ssh -s` | `sftp` (placeholder) / `git` rows | — | **`sftp` name**: declines until alkfs lands — the feature exists, the route doesn't (OQ-13 resolved: the tested decline placeholder is the shipped behavior until an alkfs-mapping ADR changes it). **git service names**: decline with reason — a subsystem request carries only a name, never a repo id (POC-3 §A.3); never half-serves |
|
| `session` → `subsystem_request` | `sftp` clients; manual `ssh -s` | `sftp` (placeholder) / `git` rows | none | **`sftp` name**: declines until alkfs lands, since the feature exists but the route doesn't (OQ-13 resolved: the tested decline placeholder is the shipped behavior until an alkfs-mapping ADR changes it). **git service names**: decline with reason, since a subsystem request carries only a name, never a repo id (POC-3 §A.3); never half-serves |
|
||||||
| `direct-tcpip` | `ssh -L`; all `ssh -D` traffic | `tunnel` | alktunnels **route 1**: adapter-side bridge — `TunnelSession::open_direct` on a held `ChannelClient`, pump `take_halves()` vs the channel | target = `SubstrateAddr::HostPort`/`Path`; `Substrate` = Tcp/Unix; accept only **after** the fabric open succeeds — failure maps to an explicit `ConnectFailed` reject (POC-3 §C.2); vanilla `-D`'s dynamic targets dispatch here (POC-3 correction 2) |
|
| `direct-tcpip` | `ssh -L`; all `ssh -D` traffic | `tunnel` | alktunnels **route 1**: adapter-side bridge, `TunnelSession::open_direct` on a held `ChannelClient`, pump `take_halves()` vs the channel | target = `SubstrateAddr::HostPort`/`Path`; `Substrate` = Tcp/Unix; accept only **after** the fabric open succeeds; failure maps to an explicit `ConnectFailed` reject (POC-3 §C.2); vanilla `-D`'s dynamic targets dispatch here (POC-3 correction 2) |
|
||||||
| `forwarded-tcpip` | `ssh -R` data channels | `tunnel` | the `register_tunnel_forwarded_openable`'s `ForwardedHandler` shape — hand the channel duplex to the handler | per fabric arrival the door opens a `forwarded-tcpip` channel **toward the vanilla client** (a vanilla client never initiates this type — sshd parity; an inbound open is the unvanilla class, declined `ConnectFailed`); echo args = the original `tcpip_forward` request's wire string verbatim + the actually-bound port (the sshd `c->path` + `get_local_port` parity — a `port = 0` mint echoes the allocated number); originator = `TunnelForwardedParams.peer`; correlation-miss → teardown-at-birth (never a half-open channel); the accept is the crate's per-session channel cap (the acceptor vocabulary) |
|
| `forwarded-tcpip` | `ssh -R` data channels | `tunnel` | the `register_tunnel_forwarded_openable_with_establisher` gate shape (alktunnels 0.5.0, review-003 W2 adoption): the router's correlation lookup rides the establisher's `ForwardedGate`; the handler's `ForwardedHandler` shape hands the channel duplex to the pump | per fabric arrival the door opens a `forwarded-tcpip` channel **toward the vanilla client** (a vanilla client never initiates this type, per sshd parity; an inbound open is the unvanilla class, declined `ConnectFailed`); echo args = the original `tcpip_forward` request's wire string verbatim + the actually-bound port (the sshd `c->path` + `get_local_port` parity; a `port = 0` mint echoes the allocated number); originator = `TunnelForwardedParams.peer`; a stale/missing correlation token fails the open **typed at the call plane** (`channel:open_failed`, `unknown_resource`) before any channel is born (the gate's miss class); the narrow gate-pass→handler-run residual race and the cap-admission declines still teardown-at-birth (never a half-open channel); the accept is the crate's per-session channel cap (the acceptor vocabulary) |
|
||||||
| `direct-streamlocal` | unix-socket forward | `tunnel` | `SubstrateAddr::Path` treatment | compiles wherever `tunnel` compiles; serving is **runtime-Unix-gated** — on Unix serves, on other platforms a clean open-decline (OQ-11 resolved: universal compile + runtime decline, no per-platform feature fork) |
|
| `direct-streamlocal` | unix-socket forward | `tunnel` | `SubstrateAddr::Path` treatment | compiles wherever `tunnel` compiles; serving is **runtime-Unix-gated**: on Unix it serves, on other platforms it is a clean open-decline (OQ-11 resolved: universal compile + runtime decline, no per-platform feature fork) |
|
||||||
| `x11` | X forwarding | — (never) | — | always declines; not feature-gated (ADR-003 §5) |
|
| `x11` | X forwarding | none (never) | none | always declines; not feature-gated (ADR-003 §5) |
|
||||||
| `auth-agent` | agent forwarding | — (never) | — | always declines; the anti-pattern of the alkvault custody posture (ADR-003 §5) |
|
| `auth-agent` | agent forwarding | none (never) | none | always declines; the anti-pattern of the alkvault custody posture (ADR-003 §5) |
|
||||||
| `session` → SOCKS5-in-channel | (no vanilla use) | — (retired, ADR-003 §1a) | — | vanilla `-D` is client-side SOCKS + `direct-tcpip`; the family local door is alksocks composed at the assembly — see `client.md` |
|
| `session` → SOCKS5-in-channel | (no vanilla use) | none (retired, ADR-003 §1a) | none | vanilla `-D` is client-side SOCKS + `direct-tcpip`; the family local door is alksocks composed at the assembly (see `client.md`) |
|
||||||
|
|
||||||
## Session-channel dispatch (ADR-006, as amended)
|
## Session-channel dispatch (ADR-006, as amended)
|
||||||
|
|
||||||
@@ -47,62 +47,64 @@ parse path: `exec_request` parses first (a git-parseable command
|
|||||||
routes to `git`; anything else to `session`'s pipe path), and
|
routes to `git`; anything else to `session`'s pipe path), and
|
||||||
`pty_request`/`shell_request` route to `session`. Request-less session
|
`pty_request`/`shell_request` route to `session`. Request-less session
|
||||||
channels decline as unfeatured. Parsing happens before dispatch and
|
channels decline as unfeatured. Parsing happens before dispatch and
|
||||||
before any resource allocation — a leaked channel alloc is a
|
before any resource allocation: a leaked channel alloc is a
|
||||||
capability leak, so **decline first, alloc never** (POC-3 stage A's
|
capability leak, so **decline first, alloc never** (POC-3 stage A's
|
||||||
decline matrix is the contract: unknown repo → collapsed generic
|
decline matrix is the contract: unknown repo → collapsed generic
|
||||||
denial *before any protocol byte* — no existence oracle; non-git
|
denial *before any protocol byte*, no existence oracle; non-git
|
||||||
command → parse-then-fail).
|
command → parse-then-fail).
|
||||||
|
|
||||||
## Global-request rows (the `-R` listen flow)
|
## Global-request rows (the `-R` listen flow)
|
||||||
|
|
||||||
`Handler::tcpip_forward(address, &mut port) -> bool` +
|
`Handler::tcpip_forward(address, &mut port) -> bool` +
|
||||||
`Handler::streamlocal_forward` (+ both cancel forms) — gate on
|
`Handler::streamlocal_forward` (+ both cancel forms), gated on
|
||||||
`tunnel` (unfeatured declines both the listen requests and the data
|
`tunnel` (unfeatured declines both the listen requests and the data
|
||||||
channels). The served flow (`w3-tunnel-listen`, decidable against
|
channels). The served flow (`w3-tunnel-listen`; the route-1 classes
|
||||||
alktunnels 0.4.1's actual surfaces with POC-3 §C.4 as the template):
|
decided against alktunnels 0.4.1's recorded surfaces, the
|
||||||
|
establisher shape against 0.5.0 (the review-003 W2 adoption), with
|
||||||
|
POC-3 §C.4 as the template):
|
||||||
|
|
||||||
- **The listen dial** — family-ward through the consumer surface:
|
- **The listen dial**, family-ward through the consumer surface:
|
||||||
`TunnelListener::open(channel_client, TunnelParams {resource: <the
|
`TunnelListener::open(channel_client, TunnelParams {resource: <the
|
||||||
wire bind endpoint>, substrate: Tcp/Unix})`. The resource is built
|
wire bind endpoint>, substrate: Tcp/Unix})`. The resource is built
|
||||||
from the wire bind endpoint: the address string rides verbatim (the
|
from the wire bind endpoint: the address string rides verbatim (the
|
||||||
OpenSSH client normalizes
|
OpenSSH client normalizes
|
||||||
the no-address case to `"localhost"` before it sends
|
the no-address case to `"localhost"` before it sends
|
||||||
(`channel_rfwd_bind_host` parity), so the door passes through), with
|
(`channel_rfwd_bind_host` parity), so the door passes through), with
|
||||||
the tcp port appended (`host:port`; `0` stays `0` — the OS-picking
|
the tcp port appended (`host:port`; `0` stays `0`, because the OS-picking
|
||||||
is the fabric's bind step, whose bound report feeds the out-param);
|
is the fabric's bind step, whose bound report feeds the out-param);
|
||||||
the unix path rides alone; whose
|
the unix path rides alone; whose
|
||||||
bind policy governs (wildcard vs loopback — the sshd `GatewayPorts`
|
bind policy governs (wildcard vs loopback, the sshd `GatewayPorts`
|
||||||
posture) is the fabric's establisher/bind closure, assembly policy
|
posture) is the fabric's establisher/bind closure, assembly policy
|
||||||
at the family's provider. The dial is bounded (`LISTEN_DIAL_TIMEOUT`
|
at the family's provider. The dial is bounded (`LISTEN_DIAL_TIMEOUT`
|
||||||
= 10 s, matching alkcall's own establisher deadline) and awaited
|
= 10 s, matching alkcall's own establisher deadline) and awaited
|
||||||
**inline** in the hook by wire necessity (the global request's
|
**inline** in the hook by wire necessity (the global request's
|
||||||
answer + the `&mut u32` out-param are the hook's own; the fabric
|
answer + the `&mut u32` out-param are the hook's own; the fabric
|
||||||
leg's answer doesn't ride this session's routing, so the stall is
|
leg's answer doesn't ride this session's routing, so the stall is
|
||||||
bounded, not deadlock-class) — recorded as the flow's one inline
|
bounded, not deadlock-class); this is recorded as the flow's one inline
|
||||||
serving step.
|
serving step.
|
||||||
- **The reply contract**: the minted `listen` token (alkcall ADR-049
|
- **The reply contract**: the minted `listen` token (alkcall ADR-049
|
||||||
amendment 3 establisher field) is REQUIRED — a producer that minted
|
amendment 3 establisher field) is REQUIRED: a producer that minted
|
||||||
no token (or no `bound` report) gets a declined listen with its
|
no token (or no `bound` report) gets a declined listen with its
|
||||||
fabric channel reaped immediately (a listen the door cannot
|
fabric channel reaped immediately (a listen the door cannot
|
||||||
correlate arrivals onto never answers success). The bound port
|
correlate arrivals onto never answers success). The bound port
|
||||||
writes the `&mut u32` out-param (`port = 0` → the actually-bound
|
writes the `&mut u32` out-param (`port = 0` → the actually-bound
|
||||||
number — sshd's `allocated_listen_port` reply parity; the
|
number; sshd's `allocated_listen_port` reply parity; the
|
||||||
`client.md` lifecycle primitive).
|
`client.md` lifecycle primitive).
|
||||||
- **The door-side arrival surface** — the fabric's bind-first accept
|
- **The door-side arrival surface**: the fabric's bind-first accept
|
||||||
dispatcher (`make_bindfirst_accept_handler`) dials the
|
dispatcher (`make_bindfirst_accept_handler`) dials the
|
||||||
`tunnel/forwarded` op **toward the door** per arrival (the
|
`tunnel/forwarded` op **toward the door** per arrival (the
|
||||||
`ForwardedOpener` closure); the door serves it through the alkcall
|
`ForwardedOpener` closure); the door serves it through the alkcall
|
||||||
openable registered into the shared per-connection registry — the
|
openable registered into the shared per-connection registry, the
|
||||||
serve-config's `TunnelListenSeam` (the `w3-seam-config` second
|
serve-config's `TunnelListenSeam` (the `w3-seam-config` second
|
||||||
course-correction: the assembly's `Arc<ChannelClient>` alone
|
course-correction: the assembly's `Arc<ChannelClient>` alone
|
||||||
carries no inbound-op serving loop; the seam fronts the
|
carries no inbound-op serving loop; the seam fronts the
|
||||||
`ServingConfig.registry` the assembly wired at the leg's
|
`ServingConfig.registry` the assembly wired at the leg's
|
||||||
`from_connection_with_serving`). The registration runs at
|
`from_connection_with_serving`). The registration runs at
|
||||||
`SshAdapter::new` (a failure is a failed construction —
|
`SshAdapter::new` (a failure is a failed construction,
|
||||||
`AdapterError::ListenRegistration`); alkcall's own gates carry the
|
`AdapterError::ListenRegistration`); alkcall's own gates carry the
|
||||||
per-identity cap + the `tunnel:forwarded` scope per arrival, then
|
per-identity cap + the `tunnel:forwarded` scope per arrival, then
|
||||||
the crate's arrival pump takes over.
|
the crate's arrival pump takes over.
|
||||||
- **The arrival pump** — per the `forwarded-tcpip` row above; unix
|
- **The arrival pump**: per the `forwarded-tcpip` row above; unix
|
||||||
substrate arrivals (a minted `streamlocal_forward` route) open
|
substrate arrivals (a minted `streamlocal_forward` route) open
|
||||||
`forwarded-streamlocal@openssh.com` with the path echoed (russh's
|
`forwarded-streamlocal@openssh.com` with the path echoed (russh's
|
||||||
server `Handle` carries the channel-open at this pin; the OQ-11
|
server `Handle` carries the channel-open at this pin; the OQ-11
|
||||||
@@ -111,13 +113,13 @@ alktunnels 0.4.1's actual surfaces with POC-3 §C.4 as the template):
|
|||||||
- **The cancel forms + bounded bookkeeping**: the per-session
|
- **The cancel forms + bounded bookkeeping**: the per-session
|
||||||
listener map (packet-loop-only, like the `channels` store) matches
|
listener map (packet-loop-only, like the `channels` store) matches
|
||||||
cancels by (the wire endpoint as the request carried it, the bound
|
cancels by (the wire endpoint as the request carried it, the bound
|
||||||
port — sshd's `c->path == host && listening_port == port` parity;
|
port; the sshd `c->path == host && listening_port == port` parity;
|
||||||
the unix forms by path alone) 1:1 into the family listener's
|
the unix forms by path alone) 1:1 into the family listener's
|
||||||
teardown (`TunnelListener::close`/`Drop` — the route leaves the
|
teardown (`TunnelListener::close`/`Drop`; the route leaves the
|
||||||
shared router *before* the teardown races), `false` for an unknown
|
shared router *before* the teardown races), `false` for an unknown
|
||||||
pair. The listen count is a bounded resource: a dedicated counter
|
pair. The listen count is a bounded resource: a dedicated counter
|
||||||
against the same `Limits::max_concurrent_channels` ceiling (the
|
against the same `Limits::max_concurrent_channels` ceiling (the
|
||||||
listener/channels mix stays unconstrained; both stay bounded — no
|
listener/channels mix stays unconstrained; both stay bounded, so no
|
||||||
unbounded listener map). A session's teardown removes its tokens
|
unbounded listener map). A session's teardown removes its tokens
|
||||||
from the shared router (a fabric arrival after the session is gone
|
from the shared router (a fabric arrival after the session is gone
|
||||||
is the correlation-miss class).
|
is the correlation-miss class).
|
||||||
@@ -127,21 +129,21 @@ alktunnels 0.4.1's actual surfaces with POC-3 §C.4 as the template):
|
|||||||
| SSH request | Family mapping |
|
| SSH request | Family mapping |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `pty_request` | `NegotiateRequest.tty = TerminalParamsWire{…}` (POC-3 §D.1: live `stty size` honored); ttymodes → the reserved modes JSON (OQ-44); a stored pty is consumed by the later `shell_request`/`exec_request` on the channel (`ssh -t host "cmd"`). A pre-serve `window_change_request` resizes the stored params (the final size wins) |
|
| `pty_request` | `NegotiateRequest.tty = TerminalParamsWire{…}` (POC-3 §D.1: live `stty size` honored); ttymodes → the reserved modes JSON (OQ-44); a stored pty is consumed by the later `shell_request`/`exec_request` on the channel (`ssh -t host "cmd"`). A pre-serve `window_change_request` resizes the stored params (the final size wins) |
|
||||||
| `shell_request` | `NegotiateRequest.cmd` = the **assembly-supplied serve-config shell argv** verbatim (no crate default; unset → the serving-path decline — failure then close; never a fallback — OQ-08 resolved; never wire-supplied). **A shell request without a preceding pty serves pipe-mode (`tty = None`)** — vanilla sshd's posture; `ssh -T` is legitimate vanilla |
|
| `shell_request` | `NegotiateRequest.cmd` = the **assembly-supplied serve-config shell argv** verbatim (no crate default; unset → the serving-path decline, failure then close; never a fallback, OQ-08 resolved; never wire-supplied). **A shell request without a preceding pty serves pipe-mode (`tty = None`)**, vanilla sshd's posture; `ssh -T` is legitimate vanilla |
|
||||||
| `exec_request` | parse → git dispatch (`git`) or alktty pipe `cmd` = exec wrapper argv with the command string appended (`wrapper ++ [cmd]`, OQ-08 — vanilla quoting semantics, never naively split; wrapper unset → the serving-path decline — failure then close; never a fallback); unknown → `channel_failure`, no resource alloc |
|
| `exec_request` | parse → git dispatch (`git`) or alktty pipe `cmd` = exec wrapper argv with the command string appended (`wrapper ++ [cmd]`, OQ-08; vanilla quoting semantics, never naively split; wrapper unset → the serving-path decline, failure then close; never a fallback); unknown → `channel_failure`, no resource alloc |
|
||||||
| `subsystem_request` | decline (per the rows table; honest, with reason) |
|
| `subsystem_request` | decline (per the rows table; honest, with reason) |
|
||||||
| `env_request` | **declined unconditionally** (OQ-09 resolved; no allowlist ships) — wire env is not family config |
|
| `env_request` | **declined unconditionally** (OQ-09 resolved; no allowlist ships); wire env is not family config |
|
||||||
| `window_change_request` | resize control through the serving bridge (POC-3 §D.1: live resize proven); fire-and-forget — no SSH reply, so a full/closed control surface drops the event |
|
| `window_change_request` | resize control through the serving bridge (POC-3 §D.1: live resize proven); fire-and-forget, no SSH reply, so a full/closed control surface drops the event |
|
||||||
| `signal` | signal control (alktty names verbatim — no door-side filter; PTY mode reaches the process group, pipe mode is pid-only — backend semantics, fidelity note, OQ-10); fire-and-forget, no decline shape |
|
| `signal` | signal control (alktty names verbatim, no door-side filter; PTY mode reaches the process group, pipe mode is pid-only; backend semantics and a fidelity note, OQ-10); fire-and-forget, no decline shape |
|
||||||
| data / `extended_data(1)` | the translation bridge (rows table) |
|
| data / `extended_data(1)` | the translation bridge (rows table) |
|
||||||
| `exit_status` | from the alktty exit chunk — sent **before** close, from the spawned task via `Session::handle()` (POC-3 §D: proven both legs). Derivation contract rules 1, 3–6 below |
|
| `exit_status` | from the alktty exit chunk, sent **before** close, from the spawned task via `Session::handle()` (POC-3 §D: proven both legs). Derivation contract rules 1, 3–6 below |
|
||||||
| `exit_signal` | negative exit-chunk codes (the Unix pipe path's signal encoding) → `exit-signal(name)` — derivation contract rule 2; `−1` → `exit-status(255)`, never a signal claim (rule 3). PTY path: signal-terminated children arrive as a plain code 1 (portable-pty's exit surface) → `exit-status(1)` — the signal-identity loss is an upstream alktty/portable-pty constraint (rule 5) |
|
| `exit_signal` | negative exit-chunk codes → `exit-signal(name)` (derivation contract rule 2); `−1` → `exit-status(255)`, never a signal claim (rule 3). Both the pipe **and** the PTY path report signal deaths as `−N` (alktty 0.8.2 reaps the PTY child directly; the former portable-pty flattening constraint is retired; rule 5) |
|
||||||
|
|
||||||
## Exit chunk → SSH exit reporting (the derivation contract)
|
## Exit chunk → SSH exit reporting (the derivation contract)
|
||||||
|
|
||||||
The alktty exit chunk (`{"type":"exit","code":N}`, exit-chunk-is-last —
|
The alktty exit chunk (`{"type":"exit","code":N}`, exit-chunk-is-last;
|
||||||
alktty ADR-004, alknet ADR-055 lineage) is the sole fidelity input; the
|
alktty ADR-004, alknet ADR-055 lineage) is the sole fidelity input; the
|
||||||
chunk is **not widened** (any widening needs its own alktty ADR — the
|
chunk is **not widened** (any widening needs its own alktty ADR; the
|
||||||
convention is consumed as-is; OQ-10 resolved). Rules, normative for the
|
convention is consumed as-is; OQ-10 resolved). Rules, normative for the
|
||||||
`exit_status`/`exit_signal` rows above:
|
`exit_status`/`exit_signal` rows above:
|
||||||
|
|
||||||
@@ -149,7 +151,7 @@ convention is consumed as-is; OQ-10 resolved). Rules, normative for the
|
|||||||
2. **Negative `−N`, N ≥ 2 (the Unix pipe path's signal encoding)** →
|
2. **Negative `−N`, N ≥ 2 (the Unix pipe path's signal encoding)** →
|
||||||
`exit-signal(name)` via a Unix signal-number→name table (server-side
|
`exit-signal(name)` via a Unix signal-number→name table (server-side
|
||||||
work; signal semantics belong to the serving platform, where the
|
work; signal semantics belong to the serving platform, where the
|
||||||
backend reaps). `core_dumped` is **never asserted** — alktty discards
|
backend reaps). `core_dumped` is **never asserted**: alktty discards
|
||||||
`ExitStatus::core_dumped()`, so nothing better is knowable from the
|
`ExitStatus::core_dumped()`, so nothing better is knowable from the
|
||||||
chunk, and SSH's `exit-signal` bool stays false. (POC-3 §D.2 observed
|
chunk, and SSH's `exit-signal` bool stays false. (POC-3 §D.2 observed
|
||||||
the full derivation live: signal TERM → `−15` → `exit-signal("TERM")`.)
|
the full derivation live: signal TERM → `−15` → `exit-signal("TERM")`.)
|
||||||
@@ -157,7 +159,7 @@ convention is consumed as-is; OQ-10 resolved). Rules, normative for the
|
|||||||
alktty's could-not-determine sentinel collides with the SIGHUP
|
alktty's could-not-determine sentinel collides with the SIGHUP
|
||||||
encoding on the Unix pipe path, and this rule accepts the fidelity
|
encoding on the Unix pipe path, and this rule accepts the fidelity
|
||||||
cost symmetrically: neither a fabricated signal for a wait failure
|
cost symmetrically: neither a fabricated signal for a wait failure
|
||||||
(a signal that never happened) nor a real one — a *genuine* SIGHUP
|
(a signal that never happened) nor a real one: a *genuine* SIGHUP
|
||||||
death on the pipe path also reports 255, not `exit-signal("HUP")`.
|
death on the pipe path also reports 255, not `exit-signal("HUP")`.
|
||||||
If the wait-failure vs SIGHUP distinction ever becomes load-bearing,
|
If the wait-failure vs SIGHUP distinction ever becomes load-bearing,
|
||||||
alktty's additive `exit_error` control type is the extension seam
|
alktty's additive `exit_error` control type is the extension seam
|
||||||
@@ -165,28 +167,31 @@ convention is consumed as-is; OQ-10 resolved). Rules, normative for the
|
|||||||
carries, with no contract change here.
|
carries, with no contract change here.
|
||||||
4. **Non-Unix**: no signal termination exists; `−1` is the only
|
4. **Non-Unix**: no signal termination exists; `−1` is the only
|
||||||
negative that can appear → rule 3.
|
negative that can appear → rule 3.
|
||||||
5. **The PTY path (fidelity limit, upstream)**: portable-pty 0.9 reaps
|
5. **The PTY path (constraint retired, alktty 0.8.2)**: portable-pty
|
||||||
a signal-terminated Unix child as a plain code 1 (the signal number
|
0.9's `ExitStatus` flattens a signal-terminated Unix child to a
|
||||||
is unrecoverable through its trait surface — verified against
|
plain code 1, so PTY-row signal deaths once arrived as
|
||||||
portable-pty 0.9.0's source) → PTY-row signal-terminated sessions
|
`exit-status(1)`. alktty 0.8.2 retires the constraint (its Unix
|
||||||
arrive as `exit-status(1)`. Faithful `exit-signal` there is an
|
PTY waiter skips portable-pty's conversion and reaps the direct
|
||||||
alktty/portable-pty ask with tty/pty-grounded justification; alkssh's
|
child via `waitpid`, decoding signal deaths as `−N`); the PTY path
|
||||||
contract adapts automatically if the convention ever holds on that
|
now reports signal termination exactly like the pipe path, and the
|
||||||
path. Pipe-mode pid-only vs PTY process-group *scope* (the `signal`
|
derivation here needed **no contract change**: the negative-code
|
||||||
|
convention is consumed verbatim (OQ-10's upstream constraint
|
||||||
|
closed). Pipe-mode pid-only vs PTY process-group *scope* (the
|
||||||
|
`signal`
|
||||||
row note) is backend semantics, not a mapping detail.
|
row note) is backend semantics, not a mapping detail.
|
||||||
6. **Unmapped negatives → rule 3.** The convention's producers are
|
6. **Unmapped negatives → rule 3.** The convention's producers are
|
||||||
`−1` and `−signum` only; a negative value with no entry in the
|
`−1` and `−signum` only; a negative value with no entry in the
|
||||||
signal-name table (defensive case — the mapping never invents a
|
signal-name table (defensive case: the mapping never invents a
|
||||||
name) reports `exit-status(255)`.
|
name) reports `exit-status(255)`.
|
||||||
|
|
||||||
## The structural constraint: spawn-and-return
|
## The structural constraint: spawn-and-return
|
||||||
|
|
||||||
russh (this pin: ADR-002) runs server handler callbacks **inline
|
russh (this pin: ADR-002) runs server handler callbacks **inline
|
||||||
inside the session's packet loop** — a blocking handler suspends all
|
inside the session's packet loop**, which means a blocking handler
|
||||||
packet routing for the connection (channel data is routed by that
|
suspends all packet routing for the connection (channel data is routed
|
||||||
same loop; an inline serve deadlocks against the peer's immediate
|
by that same loop; an inline serve deadlocks against the peer's
|
||||||
follow-up bytes — POC-3 proved by construction with receive-pack).
|
immediate follow-up bytes, as POC-3 proved by construction with
|
||||||
Therefore: **every serving path is spawned; handlers return fast**;
|
receive-pack). Therefore: **every serving path is spawned; handlers return fast**;
|
||||||
exit/status reporting from spawned tasks rides the cloned
|
exit/status reporting from spawned tasks rides the cloned
|
||||||
`Session::handle()`. This is a load-bearing correctness constraint of
|
`Session::handle()`. This is a load-bearing correctness constraint of
|
||||||
the whole spine, not an implementation choice.
|
the whole spine, not an implementation choice.
|
||||||
@@ -196,7 +201,7 @@ the whole spine, not an implementation choice.
|
|||||||
- russh knobs (`window_size`, `maximum_packet_size`,
|
- russh knobs (`window_size`, `maximum_packet_size`,
|
||||||
`channel_buffer_size`, `Limits`, `inactivity_timeout`,
|
`channel_buffer_size`, `Limits`, `inactivity_timeout`,
|
||||||
`keepalive_*`) + in-crate max-concurrent-channels cap + per-length
|
`keepalive_*`) + in-crate max-concurrent-channels cap + per-length
|
||||||
exec-parse limits — per the bounded-resources policy in
|
exec-parse limits, per the bounded-resources policy in
|
||||||
`overview.md`.
|
`overview.md`.
|
||||||
- alkcall bridge leg (when the family half is remote): channels-layer
|
- alkcall bridge leg (when the family half is remote): channels-layer
|
||||||
reassembly cap 1 MiB/channel; ADR-034 grants `alk/ssh` stream_types
|
reassembly cap 1 MiB/channel; ADR-034 grants `alk/ssh` stream_types
|
||||||
@@ -210,26 +215,26 @@ the whole spine, not an implementation choice.
|
|||||||
|
|
||||||
| Build | Serves | Declines |
|
| Build | Serves | Declines |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| none (default) | — | everything |
|
| none (default) | nothing | everything |
|
||||||
| `git` | git-parseable exec | pty/shell, plain exec, all other channel types, all `session` requests |
|
| `git` | git-parseable exec | pty/shell, plain exec, all other channel types, all `session` requests |
|
||||||
| `session` | pty/shell/pipe-exec (requires the configured shell/exec wrapper — OQ-08; an unset field declines its row though the feature is on) | git-parseable exec (the `git` parse is its gate), other channel types |
|
| `session` | pty/shell/pipe-exec (requires the configured shell/exec wrapper, OQ-08; an unset field declines its row though the feature is on) | git-parseable exec (the `git` parse is its gate), other channel types |
|
||||||
| `tunnel` | direct/forwarded-tcpip, streamlocal (unix), `-R` listens + arrivals (the listen forms additionally require the configured `TunnelListenSeam` — an egress-only door declines just the `-R` forms) | session channels (auto-reject), all others |
|
| `tunnel` | direct/forwarded-tcpip, streamlocal (unix), `-R` listens + arrivals (the listen forms additionally require the configured `TunnelListenSeam`; an egress-only door declines just the `-R` forms) | session channels (auto-reject), all others |
|
||||||
| any build + `sftp` slot | unchanged from the underlying build | `sftp` subsystems decline-with-reason until alkfs |
|
| any build + `sftp` slot | unchanged from the underlying build | `sftp` subsystems decline-with-reason until alkfs |
|
||||||
|
|
||||||
Every row is tested in *every* build it can appear in — the decline
|
Every row is tested in *every* build it can appear in: the decline
|
||||||
paths are contract territory (AGENTS.md conventions 5–6), including
|
paths are contract territory (AGENTS.md conventions 5–6), including
|
||||||
under `--all-features`.
|
under `--all-features`.
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
None open or deferred — see [open-questions.md](open-questions.md)
|
None open or deferred; see [open-questions.md](open-questions.md)
|
||||||
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-02 (session
|
(all of OQ-01…13 resolved). Doc-local contract anchors: OQ-02 (session
|
||||||
dispatch, ADR-006 as amended — §above), OQ-08 (the shell argv / exec
|
dispatch, ADR-006 as amended, §above), OQ-08 (the shell argv / exec
|
||||||
wrapper serve config — `shell_request`/`exec_request` rows and the
|
wrapper serve config, the `shell_request`/`exec_request` rows and the
|
||||||
`session→pty+shell` channel row), OQ-09 (the `env_request` row),
|
`session→pty+shell` channel row), OQ-09 (the `env_request` row),
|
||||||
OQ-10 (§the derivation contract — formalized, upstream constraint
|
OQ-10 (§the derivation contract, now formalized; the former upstream PTY
|
||||||
noted there), OQ-11 (`direct-streamlocal` row + the listen-flow gate),
|
constraint is retired as of alktty 0.8.2, rule 5), OQ-11 (`direct-streamlocal` row + the listen-flow gate),
|
||||||
OQ-13 (the `sftp` rows — the decline placeholder is the shipped
|
OQ-13 (the `sftp` rows; the decline placeholder is the shipped
|
||||||
behavior).
|
behavior).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|||||||
@@ -15,114 +15,114 @@ resolutions only; the genuinely-upstream items it surfaced (the PTY
|
|||||||
exit limit, the alkvault zeroization gap, the alkfs dependency) are
|
exit limit, the alkvault zeroization gap, the alkfs dependency) are
|
||||||
noted at the end and carry their work where their owners are.
|
noted at the end and carry their work where their owners are.
|
||||||
|
|
||||||
## Theme A — wire & family topology
|
## Theme A: wire & family topology
|
||||||
|
|
||||||
### OQ-01: ~~Does an `alk/ssh` relay/browser-leg consumer emerge?~~ — resolved
|
### OQ-01: ~~Does an `alk/ssh` relay/browser-leg consumer emerge?~~ (resolved)
|
||||||
|
|
||||||
- **Origin**: [mapping-table.md](../research/mapping-table.md) §topology, `vision.md` scope Q1
|
- **Origin**: [mapping-table.md](../research/mapping-table.md) §topology, `vision.md` scope Q1
|
||||||
- **Status**: resolved — ADR-001 is the entire decision surface; what remained was a forecast about the world, not an alkssh decision.
|
- **Status**: resolved; ADR-001 is the entire decision surface; what remained was a forecast about the world, not an alkssh decision.
|
||||||
- **Resolution**: no relay/browser-leg consumer is in scope and no crate behavior is gated on one. ADR-001 decided everything decidable now: the `alk/ssh` pin's raw-stream contract (§1), payload-blind relay as part of the contract (§2), the phase-1 edge-termination-only scope (§3), and future-leg compatibility by construction (§4). Whether a consumer ever materializes is an assembly/family use-case question — a future leg arrives via its own ADR referencing ADR-001's already-fixed contract and would exercise `client.md`'s accept surface in a new role. Nothing in the crate waits on this. (Door-type note retained for the record: the pin is one-way; consumer *existence* was never an architecture decision.)
|
- **Resolution**: no relay/browser-leg consumer is in scope and no crate behavior is gated on one. ADR-001 decided everything decidable now: the `alk/ssh` pin's raw-stream contract (§1), payload-blind relay as part of the contract (§2), the phase-1 edge-termination-only scope (§3), and future-leg compatibility by construction (§4). Whether a consumer ever materializes is an assembly/family use-case question: a future leg arrives via its own ADR referencing ADR-001's already-fixed contract and would exercise `client.md`'s accept surface in a new role. Nothing in the crate waits on this. (Door-type note retained for the record: the pin is one-way; consumer *existence* was never an architecture decision.)
|
||||||
- **Cross-references**: ADR-001, OQ-13
|
- **Cross-references**: ADR-001, OQ-13
|
||||||
|
|
||||||
## Theme B — session-channel dispatch & mapping details
|
## Theme B: session-channel dispatch & mapping details
|
||||||
|
|
||||||
### OQ-02: ~~What is the session-channel dispatch shape (SOCKS-detect vs parse, and who bounds the window)?~~ — resolved
|
### OQ-02: ~~What is the session-channel dispatch shape (SOCKS-detect vs parse, and who bounds the window)?~~ (resolved)
|
||||||
|
|
||||||
- **Origin**: [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) §Stage B.3, `mapping-table.md` §notes
|
- **Origin**: [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) §Stage B.3, `mapping-table.md` §notes
|
||||||
- **Status**: resolved. Decided by ADR-006 point 3, then simplified by the ADR-003 §1a amendment: the `dynamic-forward` feature retired (no consumer is ever the family-native SOCKS-in-channel client; vanilla `-D` is client-side SOCKS + plain `direct-tcpip`; the family `-D` door is alksocks composed locally at the assembly), so the detect lost its owner. **Session dispatch is parse-only**; request-less session channels decline. POC-3 stage B survives as the feasibility record for re-adding a remote SOCKS door via explicit ADR.
|
- **Status**: resolved. Decided by ADR-006 point 3, then simplified by the ADR-003 §1a amendment: the `dynamic-forward` feature retired (no consumer is ever the family-native SOCKS-in-channel client; vanilla `-D` is client-side SOCKS + plain `direct-tcpip`; the family `-D` door is alksocks composed locally at the assembly), so the detect lost its owner. **Session dispatch is parse-only**; request-less session channels decline. POC-3 stage B survives as the feasibility record for re-adding a remote SOCKS door via explicit ADR.
|
||||||
- **Cross-references**: ADR-006, ADR-003 §1a
|
- **Cross-references**: ADR-006, ADR-003 §1a
|
||||||
|
|
||||||
### OQ-03: ~~Username disposition~~ — resolved
|
### OQ-03: ~~Username disposition~~ (resolved)
|
||||||
|
|
||||||
- **Origin**: [poc-1-auth-bridge.md](../research/poc-1-auth-bridge.md) §2, `auth-bridge.md` §The mapping
|
- **Origin**: [poc-1-auth-bridge.md](../research/poc-1-auth-bridge.md) §2, `auth-bridge.md` §The mapping
|
||||||
- **Status**: resolved
|
- **Status**: resolved
|
||||||
- **Resolution**: usernames are never the identity model and carry no routing semantics — the fingerprint is the sole resolution path (POC-1: `ssh anything@host` works with the same identity). Whether a username string appears in logs is deployment policy, outside the crate. Rides as the recorded contract in the `auth.md` spec / ADR-004.
|
- **Resolution**: usernames are never the identity model and carry no routing semantics; the fingerprint is the sole resolution path (POC-1: `ssh anything@host` works with the same identity). Whether a username string appears in logs is deployment policy, outside the crate. Rides as the recorded contract in the `auth.md` spec / ADR-004.
|
||||||
|
|
||||||
### OQ-04: ~~OpenSSH certificate auth — is there ever a CA-trust use case?~~ — resolved
|
### OQ-04: ~~OpenSSH certificate auth, is there ever a CA-trust use case?~~ (resolved)
|
||||||
|
|
||||||
- **Origin**: `mapping-table.md` (auth rows), [poc-1-auth-bridge.md](../research/poc-1-auth-bridge.md) OQ seed 4
|
- **Origin**: `mapping-table.md` (auth rows), [poc-1-auth-bridge.md](../research/poc-1-auth-bridge.md) OQ seed 4
|
||||||
- **Status**: resolved — the decline is the settled disposition; the "is there ever" remainder was a forecast.
|
- **Status**: resolved. The decline is the settled disposition; the "is there ever" remainder was a forecast.
|
||||||
- **Resolution**: `auth_openssh_certificate` is declined unconditionally (ADR-004 decision 3: explicit reject implementation, contract-tested, POC-1-proven). There is no CA-trust story in the family identity model — exactly one SSH-shaped resolution path exists (verified key fingerprint → `IdentityProvider::resolve_from_fingerprint` → `Identity`). A certificate trust model would first require the family identity model to gain one upstream (alkcall identity territory) and would then enter alkssh as a new ADR — an auth-posture change is wire-visible. Nothing in the crate waits on this; no CA use case exists.
|
- **Resolution**: `auth_openssh_certificate` is declined unconditionally (ADR-004 decision 3: explicit reject implementation, contract-tested, POC-1-proven). There is no CA-trust story in the family identity model: exactly one SSH-shaped resolution path exists (verified key fingerprint → `IdentityProvider::resolve_from_fingerprint` → `Identity`). A certificate trust model would first require the family identity model to gain one upstream (alkcall identity territory) and would then enter alkssh as a new ADR, since an auth-posture change is wire-visible. Nothing in the crate waits on this; no CA use case exists.
|
||||||
- **Cross-references**: ADR-004, [auth.md](auth.md) §out-of-scope
|
- **Cross-references**: ADR-004, [auth.md](auth.md) §out-of-scope
|
||||||
|
|
||||||
## Theme C — auth, identity & key custody
|
## Theme C: auth, identity & key custody
|
||||||
|
|
||||||
### OQ-05: ~~What is the client-side host-key pin store?~~ — resolved (policy-seam shape)
|
### OQ-05: ~~What is the client-side host-key pin store?~~ (resolved, policy-seam shape)
|
||||||
|
|
||||||
- **Origin**: [auth-bridge.md](../research/auth-bridge.md) §Host keys, `reverse-runner.md` §5
|
- **Origin**: [auth-bridge.md](../research/auth-bridge.md) §Host keys, `reverse-runner.md` §5
|
||||||
- **Status**: resolved — the *seam* is decided; where learned pins persist is assembly detail (the crate takes a policy, not a pin store).
|
- **Status**: resolved. The *seam* is decided; where learned pins persist is assembly detail (the crate takes a policy, not a pin store).
|
||||||
- **Resolution**: two basic client cases, one seam:
|
- **Resolution**: two basic client cases, one seam:
|
||||||
|
|
||||||
1. **Known beforehand** — trust anchors exist. Family-built runners mint their host key at assembly time, so the expected fingerprint is known before first contact: assembly-supplied pins, **hard decline on mismatch** (no soft failure).
|
1. **Known beforehand**: trust anchors exist. Family-built runners mint their host key at assembly time, so the expected fingerprint is known before first contact: assembly-supplied pins, **hard decline on mismatch** (no soft failure).
|
||||||
2. **Unknown beforehand** — vanilla `sshd` on container fleets the assembly didn't provision; no anchor exists until first contact. The crate surfaces the presented host key (host, port, fingerprint) to an **assembly-provided `HostKeyPolicy`** that decides: prompt-and-record, provider-metadata match, learn-and-record, or accept-and-continue (the ephemeral-fleet shape — see below). The OpenSSH "add to known_hosts?" moment becomes an assembly policy rather than crate behavior.
|
2. **Unknown beforehand**: vanilla `sshd` on container fleets the assembly didn't provision; no anchor exists until first contact. The crate surfaces the presented host key (host, port, fingerprint) to an **assembly-provided `HostKeyPolicy`** that decides: prompt-and-record, provider-metadata match, learn-and-record, or accept-and-continue (the ephemeral-fleet shape, see below). The OpenSSH "add to known_hosts?" moment becomes an assembly policy rather than crate behavior.
|
||||||
|
|
||||||
Crate rules that make this family-shaped: the crate's default policy is **fail-closed** (reject unknown, hard-decline mismatch — russh's own reject-all default, kept); the crate never decides trust itself and never hard-codes a trust model — accept-all is not a built-in crate option, but a permissive case-2 policy is the assembly's to choose where its fleet model calls for it (alkminer's accept-all for rental fleets is that shape, made deliberately: fresh container per rent → fresh host key, so no pin can exist beforehand and the security anchor is hub-held per-instance dial keys plus what rides the tunnels, not host pins). The contract lands in the `auth.md` spec (host-key policy section) and `client.md`'s dial surface.
|
Crate rules that make this family-shaped: the crate's default policy is **fail-closed** (reject unknown, hard-decline mismatch; russh's own reject-all default, kept); the crate never decides trust itself and never hard-codes a trust model, so accept-all is not a built-in crate option, but a permissive case-2 policy is the assembly's to choose where its fleet model calls for it (alkminer's accept-all for rental fleets is that shape, made deliberately: fresh container per rent → fresh host key, so no pin can exist beforehand and the security anchor is hub-held per-instance dial keys plus what rides the tunnels, not host pins). The contract lands in the `auth.md` spec (host-key policy section) and `client.md`'s dial surface.
|
||||||
- **Cross-references**: OQ-07, `reverse-runner.md`, [poc-2-vault-keys.md](../research/poc-2-vault-keys.md) §5 (host keys ride the vault model)
|
- **Cross-references**: OQ-07, `reverse-runner.md`, [poc-2-vault-keys.md](../research/poc-2-vault-keys.md) §5 (host keys ride the vault model)
|
||||||
|
|
||||||
### OQ-06: ~~One vault, many roles?~~ — resolved
|
### OQ-06: ~~One vault, many roles?~~ (resolved)
|
||||||
|
|
||||||
- **Origin**: [poc-2-vault-keys.md](../research/poc-2-vault-keys.md) OQ seed 3
|
- **Origin**: [poc-2-vault-keys.md](../research/poc-2-vault-keys.md) OQ seed 3
|
||||||
- **Status**: resolved
|
- **Status**: resolved
|
||||||
- **Resolution**: not an alkssh constraint. The crate takes vault handles the assembly injects; whether the assembly runs separate vault instances per role (client keys vs host keys) is assembly policy. ADR-005 records the boundary: vault handle in, key material out, version semantics owned by the vault.
|
- **Resolution**: not an alkssh constraint. The crate takes vault handles the assembly injects; whether the assembly runs separate vault instances per role (client keys vs host keys) is assembly policy. ADR-005 records the boundary: vault handle in, key material out, version semantics owned by the vault.
|
||||||
|
|
||||||
### OQ-07: ~~Is the alkvault `decrypt` plain-`String` zeroization gap closed at the vault or at the adapter?~~ — resolved (at this level)
|
### OQ-07: ~~Is the alkvault `decrypt` plain-`String` zeroization gap closed at the vault or at the adapter?~~ (resolved, at this level)
|
||||||
|
|
||||||
- **Origin**: [poc-2-vault-keys.md](../research/poc-2-vault-keys.md) §2 + OQ seed 1
|
- **Origin**: [poc-2-vault-keys.md](../research/poc-2-vault-keys.md) §2 + OQ seed 1
|
||||||
- **Status**: resolved — the alkssh-level answer is the adapter-side wrap; the vault-side improvement belongs upstream, not here.
|
- **Status**: resolved. The alkssh-level answer was the adapter-side wrap; the vault-side improvement belongs upstream, not here. **Superseded upstream 2026-11-04 (alkvault 0.2.0)**: the ask shipped, `decrypt` returns `Zeroizing<String>`, zeroizing the buffer on every path including the error one, and the `rotate` path never holds a bare plaintext either.
|
||||||
- **Resolution**: **ADR-005 decision 3 is the contract on this path**: the adapter owns a `Zeroizing<String>` wrap at the seam (POC-2-proven; bounds the plaintext from adapter-takeover onward; ssh-key zeroizes the decoded key bytes on drop). The remaining unfenced pre-wrap window is **alkvault's API shape to close** — the ask is filed in alkvault's own tracker (downstream/family-space concern; alkforge and every secrets-bearing crate benefits, not just alkssh). alkssh does not carry it, does not gate on it, and ADR-005 records only the adapter obligation. When alkvault ships a `Zeroizing`-out API, ADR-005's reference here updates as a one-line doc touch, nothing else.
|
- **Resolution**: **ADR-005 decision 3 was the contract on this path**: the adapter owned a `Zeroizing<String>` wrap at the seam (POC-2-proven; bounds the plaintext from adapter-takeover onward; ssh-key zeroizes the decoded key bytes on drop). The remaining unfenced pre-wrap window was **alkvault's API shape to close**; the ask was filed in alkvault's own tracker (downstream/family-space concern; alkforge and every secrets-bearing crate benefits, not just alkssh). alkssh did not carry it, did not gate on it, and ADR-005 recorded only the adapter obligation. With alkvault 0.2.0 the window is closed upstream: the adapter-side wrap is dropped (a double wrap would be dead posture), `decrypt_and_decode` consumes the vault's zeroized return directly, and ADR-005's decision-3 pointer is amended (the predicted one-line doc touch, exactly).
|
||||||
- **Cross-references**: ADR-005 §3, OQ-05
|
- **Cross-references**: ADR-005 §3, OQ-05
|
||||||
|
|
||||||
## Theme D — session-request mapping details
|
## Theme D: session-request mapping details
|
||||||
|
|
||||||
### OQ-08: ~~Shell command convention for `shell_request`~~ — resolved
|
### OQ-08: ~~Shell command convention for `shell_request`~~ (resolved)
|
||||||
|
|
||||||
- **Origin**: `mapping-table.md` (shell row), [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) OQ seed 3
|
- **Origin**: `mapping-table.md` (shell row), [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) OQ seed 3
|
||||||
- **Status**: resolved — assembly-supplied serve-config field; no crate default.
|
- **Status**: resolved; assembly-supplied serve-config field, no crate default.
|
||||||
- **Resolution**: the shell convention is **assembly-supplied serve configuration** — the crate bakes no default, and each unset field means its request **declines with a clean error** (never a silent fallback). Grounding for the shape: alktty's contract requires a non-empty `cmd` vector (there is no pass-through-absence seam at `drive_session`), the wire supplies nothing (`shell_request` carries no command), and the crate is not an sshd — it has no account/passwd model from which "the user's shell" could be derived, so a crate-level default (`bash`, `sh`) would be deployment policy smuggled into the crate (the composability boundary, `overview.md`). Two fields, each governing its own row:
|
- **Resolution**: the shell convention is **assembly-supplied serve configuration**: the crate bakes no default, and each unset field means its request **declines with a clean error** (never a silent fallback). Grounding for the shape: alktty's contract requires a non-empty `cmd` vector (there is no pass-through-absence seam at `drive_session`), the wire supplies nothing (`shell_request` carries no command), and the crate is not an sshd and has no account/passwd model from which "the user's shell" could be derived, so a crate-level default (`bash`, `sh`) would be deployment policy smuggled into the crate (the composability boundary, `overview.md`). Two fields, each governing its own row:
|
||||||
|
|
||||||
1. **shell argv** — what `shell_request` runs verbatim as `NegotiateRequest.cmd`.
|
1. **shell argv**: what `shell_request` runs verbatim as `NegotiateRequest.cmd`.
|
||||||
2. **exec wrapper argv** (e.g. `["/bin/sh", "-c"]`) — what a non-git `exec_request` translates to: the wire's single command string is **appended as the wrapper argv's final element** (`wrapper ++ [cmd]`), preserving vanilla `ssh host "cmd"` quoting/expansion semantics — command strings are never naively split (that would break quoting). The wrapper form is platform-neutral: the assembly configures whatever its fleet's shell is.
|
2. **exec wrapper argv** (e.g. `["/bin/sh", "-c"]`): what a non-git `exec_request` translates to. The wire's single command string is **appended as the wrapper argv's final element** (`wrapper ++ [cmd]`), preserving vanilla `ssh host "cmd"` quoting/expansion semantics; command strings are never naively split (that would break quoting). The wrapper form is platform-neutral: the assembly configures whatever its fleet's shell is.
|
||||||
|
|
||||||
Unset field → its row declines; a `session` build with no configured shell serves neither shell nor exec (fail-closed — no half-capability where the feature is on). The git row is unaffected (parse to `alkgit` first — it never needs a shell). `cwd`/`env` in `NegotiateRequest` are assembly-supplied serve-config values, never wire-derived (`env_request` stays declined — OQ-09). Recorded on `mapping.md`'s `shell_request`/`exec_request` rows.
|
Unset field → its row declines; a `session` build with no configured shell serves neither shell nor exec (fail-closed, no half-capability where the feature is on). The git row is unaffected (parse to `alkgit` first, since it never needs a shell). `cwd`/`env` in `NegotiateRequest` are assembly-supplied serve-config values, never wire-derived (`env_request` stays declined, OQ-09). Recorded on `mapping.md`'s `shell_request`/`exec_request` rows.
|
||||||
- **Cross-references**: OQ-09, [mapping.md](mapping.md)
|
- **Cross-references**: OQ-09, [mapping.md](mapping.md)
|
||||||
|
|
||||||
### OQ-09: ~~`env_request` policy — is an allowlist ever wanted?~~ — resolved
|
### OQ-09: ~~`env_request` policy, is an allowlist ever wanted?~~ (resolved)
|
||||||
|
|
||||||
- **Origin**: `mapping-table.md` (env_request row)
|
- **Origin**: `mapping-table.md` (env_request row)
|
||||||
- **Status**: resolved — decline-by-default is the fixed contract; no allowlist ships.
|
- **Status**: resolved. Decline-by-default is the fixed contract; no allowlist ships.
|
||||||
- **Resolution**: `env_request` is declined **unconditionally**. Wire-supplied environment is not family config: session env comes from assembly serve configuration (`NegotiateRequest.env`), never from the wire. The only open shape was whether an allowlist ships — it does not; a future allowlist would be a new ADR (a serving capability is wire-visible) on a concrete consumer need, of which none exists.
|
- **Resolution**: `env_request` is declined **unconditionally**. Wire-supplied environment is not family config: session env comes from assembly serve configuration (`NegotiateRequest.env`), never from the wire. The only open shape was whether an allowlist ships; it does not, and a future allowlist would be a new ADR (a serving capability is wire-visible) on a concrete consumer need, of which none exists.
|
||||||
- **Cross-references**: OQ-08, [mapping.md](mapping.md) (`env_request` row)
|
- **Cross-references**: OQ-08, [mapping.md](mapping.md) (`env_request` row)
|
||||||
|
|
||||||
### OQ-10: ~~SSH `exit-signal` fidelity over the alktty exit-code convention~~ — resolved (closed as contract + upstream constraint)
|
### OQ-10: ~~SSH `exit-signal` fidelity over the alktty exit-code convention~~ (resolved: closed as contract + upstream constraint)
|
||||||
|
|
||||||
- **Origin**: `mapping-table.md` (exit_signal row), [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) §D.2. **Premise corrected 2026-10-27** (with the alktty side): the POC seed's "the exit chunk carries no signal identity — alktty might widen the chunk" framing was wrong; the negative code *is* the signal encoding. Integrated into the seed rather than argued after the fact.
|
- **Origin**: `mapping-table.md` (exit_signal row), [poc-3-mapping-bakeoff.md](../research/poc-3-mapping-bakeoff.md) §D.2. **Premise corrected 2026-10-27** (with the alktty side): the POC seed's "the exit chunk carries no signal identity (alktty might widen the chunk)" framing was wrong; the negative code *is* the signal encoding. Integrated into the seed rather than argued after the fact.
|
||||||
- **Status**: resolved — as asked, this was never a decision to make: the convention alkssh consumes does not change (alktty's exit-chunk convention, alktty ADR-004 / alknet ADR-055 lineage — no widening proposed or justified; "easier for alkssh" is not a tty/pty-grounded justification, and any widening ripples `ControlMessage::Exit`'s parsed shape, the backend API, and that ADR lineage, so it needs its own alktty ADR). What the entry surfaced was (a) a *derivation contract* on alkssh's side and (b) an upstream *fidelity limitation* — both now recorded where they constrain.
|
- **Status**: resolved. As asked, this was never a decision to make: the convention alkssh consumes does not change (alktty's exit-chunk convention, alktty ADR-004 / alknet ADR-055 lineage; no widening proposed or justified). "Easier for alkssh" is not a tty/pty-grounded justification, and any widening ripples `ControlMessage::Exit`'s parsed shape, the backend API, and that ADR lineage, so it needs its own alktty ADR. What the entry surfaced was (a) a *derivation contract* on alkssh's side and (b) an upstream *fidelity limitation*, both now recorded where they constrain.
|
||||||
- **Resolution**:
|
- **Resolution**:
|
||||||
- **The derivation contract** is formalized as `mapping.md` §"Exit chunk → SSH exit reporting" (the contract behind the `exit_status`/`exit_signal` rows): `code ≥ 0` → `exit-status(N)`; negative `−N` on the Unix pipe path → `exit-signal(name)` via a server-side Unix signal-number→name table; `−1` → `exit-status(255)` and never a signal claim (the sentinel collides with SIGHUP; fabricating `exit-signal("HUP")` for a wait failure invents a signal that never happened); non-Unix negatives → the `−1` rule; `core_dumped` never asserted. POC-3 §D.2 observed the pipe-path derivation live (`−15` → `exit-signal("TERM")`).
|
- **The derivation contract** is formalized as `mapping.md` §"Exit chunk → SSH exit reporting" (the contract behind the `exit_status`/`exit_signal` rows): `code ≥ 0` → `exit-status(N)`; negative `−N` on the Unix pipe path → `exit-signal(name)` via a server-side Unix signal-number→name table; `−1` → `exit-status(255)` and never a signal claim (the sentinel collides with SIGHUP; fabricating `exit-signal("HUP")` for a wait failure invents a signal that never happened); non-Unix negatives → the `−1` rule; `core_dumped` never asserted. POC-3 §D.2 observed the pipe-path derivation live (`−15` → `exit-signal("TERM")`).
|
||||||
- **The upstream constraint**: the PTY path loses the signal number (portable-pty 0.9 reaps a signal-terminated Unix child as a plain code 1 through its trait surface — verified against its source) → PTY-row signal-terminated sessions arrive as `exit-status(1)`. Faithful `exit-signal` on the PTY row is an **alktty/portable-pty ask with tty/pty-grounded justification**; alkssh's mapping adapts automatically with no contract change if the convention ever holds on that path. Recorded as rule 5 of `mapping.md`'s derivation contract (not tracked here — it constrains alktty, not alkssh).
|
- **The upstream constraint** (superseded 2026-11-04, alktty 0.8.2): the PTY path used to lose the signal number (portable-pty 0.9 reaps a signal-terminated Unix child as a plain code 1 through its trait surface; verified against its source). alktty 0.8.2 retired that constraint consumer-input-driven (alktty review 004, this entry's bookkeeping): its Unix PTY waiter now reaps the direct child via `waitpid` and decodes signal deaths as `−N`, so the convention holds on both paths and the PTY-row signal-terminated session reports `exit-signal(name)` with **no alkssh contract change** (the negative code is consumed verbatim; `mapping.md` rule 5 amended). The `-1` sentinel collision (SIGHUP vs wait-failure) remains on the pipe path and gains the cancel path; the rule-3 reading stands unchanged.
|
||||||
- **Cross-references**: OQ-08, [mapping.md](mapping.md) (derivation contract + `exit_signal`/`exit_status`/`signal` rows), ADR-006, alktty ADR-004 / alknet ADR-055
|
- **Cross-references**: OQ-08, [mapping.md](mapping.md) (derivation contract + `exit_signal`/`exit_status`/`signal` rows), ADR-006, alktty ADR-004 / alknet ADR-055
|
||||||
|
|
||||||
### OQ-11: ~~`direct-streamlocal` platform surface — compile-time or runtime decline shape?~~ — resolved
|
### OQ-11: ~~`direct-streamlocal` platform surface, compile-time or runtime decline shape?~~ (resolved)
|
||||||
|
|
||||||
- **Origin**: `mapping-table.md` (streamlocal row)
|
- **Origin**: `mapping-table.md` (streamlocal row)
|
||||||
- **Status**: resolved — universal compile, runtime decline.
|
- **Status**: resolved: universal compile, runtime decline.
|
||||||
- **Resolution**: `direct-streamlocal` compiles wherever `tunnel` compiles; the serve path is **runtime-Unix-gated** — on Unix it serves (`SubstrateAddr::Path` treatment), on every other platform it declines with a clean SSH open-failure (never a half-open). This wins over a unix-only compile split: no per-platform feature fork (the feature graph stays flat and platform-independent — ADR-003), the decline matrix stays uniform across builds, and the decline path is contract-tested in every build like every other decline (fail-closed posture). The research row's "compile-time feature + runtime decline" hybrid collapses to the runtime-decline half — no platform-conditional feature is needed.
|
- **Resolution**: `direct-streamlocal` compiles wherever `tunnel` compiles; the serve path is **runtime-Unix-gated** (on Unix it serves with the `SubstrateAddr::Path` treatment, on every other platform it declines with a clean SSH open-failure, never a half-open). This wins over a unix-only compile split: no per-platform feature fork (the feature graph stays flat and platform-independent, ADR-003), the decline matrix stays uniform across builds, and the decline path is contract-tested in every build like every other decline (fail-closed posture). The research row's "compile-time feature + runtime decline" hybrid collapses to the runtime-decline half; no platform-conditional feature is needed.
|
||||||
- **Cross-references**: ADR-003, [mapping.md](mapping.md) (`direct-streamlocal` row + the `-R` listen flow)
|
- **Cross-references**: ADR-003, [mapping.md](mapping.md) (`direct-streamlocal` row + the `-R` listen flow)
|
||||||
|
|
||||||
### OQ-12: ~~Client reconnect: does the crate own forward/session restoration?~~ — resolved
|
### OQ-12: ~~Client reconnect: does the crate own forward/session restoration?~~ (resolved)
|
||||||
|
|
||||||
- **Origin**: `reverse-runner.md` §4
|
- **Origin**: `reverse-runner.md` §4
|
||||||
- **Status**: resolved
|
- **Status**: resolved
|
||||||
- **Resolution**: the crate owns lifecycle primitives only — bound-port handles (forward listeners return the actually-bound port, per russh's `tcpip_forward -> u32`), explicit cancellable forward/session handles, and clean disconnects. Reconnect-and-restore policy (rebinding persisted tunnel ports per alkminer's flow) is assembly-owned. Recorded as a constraint in `client.md` §Lifecycle primitives; no reconnect policy is baked into the crate.
|
- **Resolution**: the crate owns lifecycle primitives only: bound-port handles (forward listeners return the actually-bound port, per russh's `tcpip_forward -> u32`), explicit cancellable forward/session handles, and clean disconnects. Reconnect-and-restore policy (rebinding persisted tunnel ports per alkminer's flow) is assembly-owned. Recorded as a constraint in `client.md` §Lifecycle primitives; no reconnect policy is baked into the crate.
|
||||||
- **Cross-references**: [client.md](client.md), `reverse-runner.md`
|
- **Cross-references**: [client.md](client.md), `reverse-runner.md`
|
||||||
|
|
||||||
## Theme E — future features
|
## Theme E: future features
|
||||||
|
|
||||||
### OQ-13: ~~The `sftp` client+server halves when alkfs lands~~ — resolved
|
### OQ-13: ~~The `sftp` client+server halves when alkfs lands~~ (resolved)
|
||||||
|
|
||||||
- **Origin**: `mapping-table.md` (sftp rows), `reverse-runner.md`, POC-2 scope notes
|
- **Origin**: `mapping-table.md` (sftp rows), `reverse-runner.md`, POC-2 scope notes
|
||||||
- **Status**: resolved — the alkssh-side answer is fully decided; the remainder belongs to alkfs, not to this tracker.
|
- **Status**: resolved. The alkssh-side answer is fully decided; the remainder belongs to alkfs, not to this tracker.
|
||||||
- **Resolution**: the `sftp` feature ships as the **tested decline placeholder in both directions** from the first build — the feature exists in the graph (ADR-003), advertisement is honest, and the decline contract is tested on both sides (`mapping.md`/`client.md`). That *is* the shipped behavior for as long as alkfs is pending, and it needs no further decision. What remained "pending" was the eventual alkfs-protocol mapping — a decision downstream of alkfs' design (family ADR-002; alkssh follows alkfs' maturity): alkssh writes that mapping and its ADR when the target protocol exists to map, the same way every other row's mapping was decided before implementation. No alkssh decision waits on this; there is no alkssh-side sftp work.
|
- **Resolution**: the `sftp` feature ships as the **tested decline placeholder in both directions** from the first build: the feature exists in the graph (ADR-003), advertisement is honest, and the decline contract is tested on both sides (`mapping.md`/`client.md`). That *is* the shipped behavior for as long as alkfs is pending, and it needs no further decision. What remained "pending" was the eventual alkfs-protocol mapping, a decision downstream of alkfs' design (family ADR-002; alkssh follows alkfs' maturity): alkssh writes that mapping and its ADR when the target protocol exists to map, the same way every other row's mapping was decided before implementation. No alkssh decision waits on this; there is no alkssh-side sftp work.
|
||||||
- **Cross-references**: OQ-01, ADR-003, [client.md](client.md) §op rows, family ADR-002
|
- **Cross-references**: OQ-01, ADR-003, [client.md](client.md) §op rows, family ADR-002
|
||||||
|
|
||||||
## Upstream watch items (not OQs)
|
## Upstream watch items (not OQs)
|
||||||
@@ -130,13 +130,15 @@ noted at the end and carry their work where their owners are.
|
|||||||
Recorded so the dependency is visible, with the work carried where
|
Recorded so the dependency is visible, with the work carried where
|
||||||
their owners are:
|
their owners are:
|
||||||
|
|
||||||
- **PTY-path exit fidelity** — needs alktty/portable-pty work (with
|
- **alkfs landing**: the `sftp` placeholder's eventual replacement
|
||||||
tty/pty-grounded justification) before faithful `exit-signal` can
|
|
||||||
hold on the PTY row; alkssh adapts with no contract change
|
|
||||||
(`mapping.md` derivation contract, rule 5; OQ-10).
|
|
||||||
- **alkfs landing** — the `sftp` placeholder's eventual replacement
|
|
||||||
(family ADR-002); alkssh writes the sftp mapping + ADR when the
|
(family ADR-002); alkssh writes the sftp mapping + ADR when the
|
||||||
protocol exists (OQ-13).
|
protocol exists (OQ-13).
|
||||||
- **alkvault `Zeroizing`-out `decrypt`** — closes the pre-wrap
|
- ~~**PTY-path exit fidelity**~~: closed upstream (alktty 0.8.2,
|
||||||
plaintext window upstream (OQ-07 / ADR-005 §3); a one-line doc touch
|
review 004's resolution): the PTY waiter now reports signal deaths
|
||||||
for alkssh when it ships.
|
as `−N`; the derivation contract needed no alkssh change
|
||||||
|
(`mapping.md` rule 5 amended; OQ-10).
|
||||||
|
- ~~**alkvault `Zeroizing`-out `decrypt`**~~: closed upstream
|
||||||
|
(alkvault 0.2.0, review 001's resolution; also closes the `rotate`
|
||||||
|
window and zeroizes the error path): the adapter seam wrap is
|
||||||
|
dropped, ADR-005 decision 3 + OQ-07 amended as the predicted
|
||||||
|
one-line doc touch.
|
||||||
@@ -3,7 +3,7 @@ status: reviewed
|
|||||||
last_updated: 2026-10-28
|
last_updated: 2026-10-28
|
||||||
---
|
---
|
||||||
|
|
||||||
# Overview — alkssh
|
# Overview: alkssh
|
||||||
|
|
||||||
## What this crate is
|
## What this crate is
|
||||||
|
|
||||||
@@ -13,18 +13,18 @@ bridges to/from family protocols. It is **a door in both directions,
|
|||||||
vanilla on the outside** (vision principle 2; ADR framing in
|
vanilla on the outside** (vision principle 2; ADR framing in
|
||||||
`mapping.md` / `client.md`):
|
`mapping.md` / `client.md`):
|
||||||
|
|
||||||
- **Server direction** — a door *for vanilla clients*: vanilla `ssh`,
|
- **Server direction**: a door *for vanilla clients*. Vanilla `ssh`,
|
||||||
`git`, and (never for us, by design) sftp tooling connect as they
|
`git`, and (never for us, by design) sftp tooling connect as they
|
||||||
would to any sshd; each accepted SSH channel/request is mapped onto
|
would to any sshd; each accepted SSH channel/request is mapped onto
|
||||||
exactly one family protocol seam and exactly one cargo feature.
|
exactly one family protocol seam and exactly one cargo feature.
|
||||||
Primary current purpose: the alkforge git door.
|
Primary current purpose: the alkforge git door.
|
||||||
- **Client direction** — a door *to vanilla servers*: a family app
|
- **Client direction**: a door *to vanilla servers*. A family app
|
||||||
(the reverse-runner hub) dials vanilla `sshd` (rented fleets,
|
(the reverse-runner hub) dials vanilla `sshd` (rented fleets,
|
||||||
runpod/vast.ai-style boxes) and the ops map family-side onto
|
runpod/vast.ai-style boxes) and the ops map family-side onto
|
||||||
alktty/alktunnels/alkgit types; the runner stays vanilla by design.
|
alktty/alktunnels/alkgit types; the runner stays vanilla by design.
|
||||||
|
|
||||||
The crate's substance is glue and mapping; no SSH framing is authored
|
The crate's substance is glue and mapping; no SSH framing is authored
|
||||||
in-family (family ADR-003 decision 1) — russh owns the wire, and the
|
in-family (family ADR-003 decision 1); russh owns the wire, and the
|
||||||
pin is ADR-002.
|
pin is ADR-002.
|
||||||
|
|
||||||
## Position in the family
|
## Position in the family
|
||||||
@@ -35,17 +35,17 @@ pin is ADR-002.
|
|||||||
| SSH channel-type → family-protocol mapping | **this crate** (the spine, `mapping.md` / `client.md`) |
|
| SSH channel-type → family-protocol mapping | **this crate** (the spine, `mapping.md` / `client.md`) |
|
||||||
| Identity (fingerprint → `Identity`) | alkcall `IdentityProvider`, assembly-implemented; never the vault (vault = secrets *we* hold) |
|
| Identity (fingerprint → `Identity`) | alkcall `IdentityProvider`, assembly-implemented; never the vault (vault = secrets *we* hold) |
|
||||||
| Secrets we hold (client dial keys, host keys) | alkvault-backed sources (ADR-005) |
|
| Secrets we hold (client dial keys, host keys) | alkvault-backed sources (ADR-005) |
|
||||||
| The `alk/ssh` ALPN | ADR-001 (edge-termination contract; relay/browser legs have no consumer in scope — OQ-01 resolved) |
|
| The `alk/ssh` ALPN | ADR-001 (edge-termination contract; relay/browser legs have no consumer in scope, OQ-01 resolved) |
|
||||||
| Deployment policy (which features, which ACLs) | the embedding assembly (alkforge) |
|
| Deployment policy (which features, which ACLs) | the embedding assembly (alkforge) |
|
||||||
|
|
||||||
Composability boundary: alkssh is a door, not a backend — it depends
|
Composability boundary: alkssh is a door, not a backend. It depends
|
||||||
on alkcall types at the wire layer and the mapped crates' session or
|
on alkcall types at the wire layer and the mapped crates' session or
|
||||||
client surfaces, and carries no deployment policy. A downstream app
|
client surfaces, and carries no deployment policy. A downstream app
|
||||||
embeds it, enables features (ADR-003), brings identity/ACL config.
|
embeds it, enables features (ADR-003), brings identity/ACL config.
|
||||||
|
|
||||||
## Public surface (categories)
|
## Public surface (categories)
|
||||||
|
|
||||||
The crate is a library (`[lib]`, no binary — non-goal); the surface
|
The crate is a library (`[lib]`, no binary, a stated non-goal); the surface
|
||||||
categories the phase-1 specs define:
|
categories the phase-1 specs define:
|
||||||
|
|
||||||
- **Server stack** (`mapping.md`): serve configuration (feature-gated
|
- **Server stack** (`mapping.md`): serve configuration (feature-gated
|
||||||
@@ -55,7 +55,7 @@ categories the phase-1 specs define:
|
|||||||
- **Client stack** (`client.md`): dial surface (address, key source,
|
- **Client stack** (`client.md`): dial surface (address, key source,
|
||||||
host-key policy, keepalive/timeout config), the op-row consumers
|
host-key policy, keepalive/timeout config), the op-row consumers
|
||||||
(session exec/pty, tunnel dial, `-R` listen + acceptor), and
|
(session exec/pty, tunnel dial, `-R` listen + acceptor), and
|
||||||
lifecycle primitives (bound-port handles, cancel — OQ-12: no
|
lifecycle primitives (bound-port handles, cancel; OQ-12: no
|
||||||
reconnect policy is owned).
|
reconnect policy is owned).
|
||||||
- **Key sources** (`auth.md`): vault-backed source construction per
|
- **Key sources** (`auth.md`): vault-backed source construction per
|
||||||
ADR-005 for both directions (client keys *and* host keys).
|
ADR-005 for both directions (client keys *and* host keys).
|
||||||
@@ -63,7 +63,7 @@ categories the phase-1 specs define:
|
|||||||
at the server; the identity→seam propagation for mapped channels.
|
at the server; the identity→seam propagation for mapped channels.
|
||||||
|
|
||||||
Feature graph (ADR-003): `session`, `git`, `tunnel`, `sftp`
|
Feature graph (ADR-003): `session`, `git`, `tunnel`, `sftp`
|
||||||
(placeholder) — flat, independent, `default = []` (no capabilities),
|
(placeholder); flat, independent, `default = []` (no capabilities),
|
||||||
two-direction symmetry, fail-closed declines contract-tested on every
|
two-direction symmetry, fail-closed declines contract-tested on every
|
||||||
side of every feature.
|
side of every feature.
|
||||||
|
|
||||||
@@ -82,17 +82,17 @@ surface notes):
|
|||||||
| Keepalives (client direction, cloud fleets) | `keepalive_interval`/`keepalive_max` | 15 s / 3 (reverse-runner evidence: NAT'd runners) |
|
| Keepalives (client direction, cloud fleets) | `keepalive_interval`/`keepalive_max` | 15 s / 3 (reverse-runner evidence: NAT'd runners) |
|
||||||
| Concurrent channels per session | ours (in-crate) + per-identity policy when bridged | ours to cap |
|
| Concurrent channels per session | ours (in-crate) + per-identity policy when bridged | ours to cap |
|
||||||
|
|
||||||
No unbounded loops, no unbounded buffers — the fail-closed declines
|
No unbounded loops, no unbounded buffers; the fail-closed declines
|
||||||
plus the knobs above are the whole posture.
|
plus the knobs above are the whole posture.
|
||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
Per `vision.md`: no in-family SSH protocol machinery (ever); not an
|
Per `vision.md`: no in-family SSH protocol machinery (ever); not an
|
||||||
sshd (no local accounts, no OpenSSH `authorized_keys` semantics, no
|
sshd (no local accounts, no OpenSSH `authorized_keys` semantics, no
|
||||||
PAM); no sftp serving until alkfs lands (OQ-13 resolved — the feature
|
PAM); no sftp serving until alkfs lands (OQ-13 resolved; the feature
|
||||||
exists as a tested declining placeholder); no binary/CLI; no password
|
exists as a tested declining placeholder); no binary/CLI; no password
|
||||||
or keyboard-interactive auth in either direction; no x11 or agent
|
or keyboard-interactive auth in either direction; no x11 or agent
|
||||||
forwarding at all (not even feature-gated — ADR-003 §5).
|
forwarding at all (not even feature-gated, ADR-003 §5).
|
||||||
|
|
||||||
## Lifecycle note
|
## Lifecycle note
|
||||||
|
|
||||||
@@ -101,13 +101,13 @@ This crate follows the family crate template (alktty root pattern):
|
|||||||
API = crate-root re-exports. The dependency set (russh per ADR-002,
|
API = crate-root re-exports. The dependency set (russh per ADR-002,
|
||||||
alkcall types, per-feature sibling deps: alktty/alktunnels/alkgit) and
|
alkcall types, per-feature sibling deps: alktty/alktunnels/alkgit) and
|
||||||
the manifest (`rust-version` 1.89, edition 2024) land at
|
the manifest (`rust-version` 1.89, edition 2024) land at
|
||||||
implementation start — ADR-002 records the manifest decisions.
|
implementation start; ADR-002 records the manifest decisions.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [README.md](README.md) (index), [open-questions.md](open-questions.md) (OQ-01…13)
|
- [README.md](README.md) (index), [open-questions.md](open-questions.md) (OQ-01…13)
|
||||||
- ADRs 001–006 in [decisions/](decisions/)
|
- ADRs 001–006 in [decisions/](decisions/)
|
||||||
- `docs/research/` — vision, POC findings (1/2/3), russh inventory,
|
- `docs/research/`: vision, POC findings (1/2/3), russh inventory,
|
||||||
auth bridge, mapping table (validated), reverse-runner extraction
|
auth bridge, mapping table (validated), reverse-runner extraction
|
||||||
- Family ADR-003 (the structural decision), family ADR-002 (alkforge;
|
- Family ADR-003 (the structural decision), family ADR-002 (alkforge;
|
||||||
why exclusion matters)
|
why exclusion matters)
|
||||||
+120
-120
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
status: closed
|
status: closed
|
||||||
last_updated: 2026-10-30 (the plan closed at the publish of alkssh
|
last_updated: 2026-10-30 (the plan closed at the publish of alkssh
|
||||||
0.1.0 — tag `alkssh-v0.1.0`; waves 1–5 decomposed + implemented +
|
0.1.0, tag `alkssh-v0.1.0`; waves 1–5 decomposed + implemented +
|
||||||
gated 2026-10-28/30)
|
gated 2026-10-28/30)
|
||||||
---
|
---
|
||||||
|
|
||||||
# alkssh — Implementation plan
|
# alkssh: Implementation plan
|
||||||
|
|
||||||
Wave-based decomposition of the architecture (`docs/architecture/`)
|
Wave-based decomposition of the architecture (`docs/architecture/`)
|
||||||
into units of implementable work. The sibling pattern proven on
|
into units of implementable work. The sibling pattern proven on
|
||||||
@@ -15,7 +15,7 @@ shape of later ones (wave 1's config/error/key-source types are what
|
|||||||
every later task implements against; wave 2's server stack is what
|
every later task implements against; wave 2's server stack is what
|
||||||
every mapping composes into), and decomposing only the next wave at a
|
every mapping composes into), and decomposing only the next wave at a
|
||||||
time keeps each session's task set reviewable and lets later
|
time keeps each session's task set reviewable and lets later
|
||||||
decompositions absorb earlier waves' course corrections — reviews
|
decompositions absorb earlier waves' course corrections: reviews
|
||||||
sometimes change things, and tasks downstream then have to be edited,
|
sometimes change things, and tasks downstream then have to be edited,
|
||||||
so decomposing too far ahead is paid rework.
|
so decomposing too far ahead is paid rework.
|
||||||
|
|
||||||
@@ -26,16 +26,16 @@ branches from stepping on each other). Wave boundaries are also review
|
|||||||
boundaries. Each wave's section here carries a **decomposition record**
|
boundaries. Each wave's section here carries a **decomposition record**
|
||||||
added when that wave actually decomposes (not written upfront).
|
added when that wave actually decomposes (not written upfront).
|
||||||
|
|
||||||
Scope authority: **v0.1.0 publishes the crate** — the complete
|
Scope authority: **v0.1.0 publishes the crate**: the complete
|
||||||
two-direction door (server stack + all mappings, client stack + op
|
two-direction door (server stack + all mappings, client stack + op
|
||||||
rows, key sources, the decline matrix contract-tested per build). The
|
rows, key sources, the decline matrix contract-tested per build). The
|
||||||
sftp decline placeholder *is* shipped behavior (OQ-13 resolved). **The
|
sftp decline placeholder *is* shipped behavior (OQ-13 resolved). **The
|
||||||
assembly stays out of these waves**: alkforge's mounting of
|
assembly stays out of these waves**: alkforge's mounting of
|
||||||
`SshAdapter::handle` at an alkcall `channel/open` boundary, its
|
`SshAdapter::handle` at an alkcall `channel/open` boundary, its
|
||||||
feature/ACL selection, its `IdentityProvider` and serve-config values
|
feature/ACL selection, its `IdentityProvider` and serve-config values
|
||||||
are downstream composition work — the crate ships the door, not the
|
are downstream composition work; the crate ships the door, not the
|
||||||
deployment. No binary, no workspace split (single crate, the alktty
|
deployment. No binary, no workspace split (single crate, the alktty
|
||||||
root pattern; no tracks — one crate, one publish).
|
root pattern; no tracks: one crate, one publish).
|
||||||
|
|
||||||
## The waves
|
## The waves
|
||||||
|
|
||||||
@@ -44,69 +44,69 @@ mappings ∥ client direction) → validation & release.
|
|||||||
|
|
||||||
| Wave | Contents | Depends on | Status |
|
| Wave | Contents | Depends on | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| [1](#wave-1--foundations) | Manifest landing (russh pin, feature graph); error model; config surface (serve-config + dial-config + `Limits`); vault-backed key sources; host-key policy seam; fingerprint; test doubles | — | decomposed |
|
| [1](#wave-1-foundations) | Manifest landing (russh pin, feature graph); error model; config surface (serve-config + dial-config + `Limits`); vault-backed key sources; host-key policy seam; fingerprint; test doubles | none | decomposed |
|
||||||
| [2](#wave-2--the-server-stack) | `SshAdapter::handle` mount + `Config` construction; the auth bridge; parse-only dispatch + fail-closed decline core; spawn-and-return plumbing; bounded resources; loopback harness | wave 1 | decomposed |
|
| [2](#wave-2-the-server-stack) | `SshAdapter::handle` mount + `Config` construction; the auth bridge; parse-only dispatch + fail-closed decline core; spawn-and-return plumbing; bounded resources; loopback harness | wave 1 | decomposed |
|
||||||
| [3](#wave-3--server-mappings) | session → alktty (translation bridge + exit derivation); git → alkgit (exec grammar + dispatch); tunnel → alktunnels route 1; sftp placeholder declines | wave 2 | decomposed |
|
| [3](#wave-3-server-mappings) | session → alktty (translation bridge + exit derivation); git → alkgit (exec grammar + dispatch); tunnel → alktunnels route 1; sftp placeholder declines | wave 2 | decomposed |
|
||||||
| [4](#wave-4--client-direction) | Dial surface; op rows (exec/pty, direct-tcpip, git exec); the `-R` leg (listen + acceptor registry); lifecycle primitives | wave 1 | decomposed |
|
| [4](#wave-4-client-direction) | Dial surface; op rows (exec/pty, direct-tcpip, git exec); the `-R` leg (listen + acceptor registry); lifecycle primitives | wave 1 | decomposed |
|
||||||
| [5](#wave-5--validation--release) | Decline-matrix contract suite; real-tool e2e; fuzzing; docs; **the publish** | waves 3 + 4 | published |
|
| [5](#wave-5-validation--release) | Decline-matrix contract suite; real-tool e2e; fuzzing; docs; **the publish** | waves 3 + 4 | published |
|
||||||
|
|
||||||
## Wave 1 — Foundations
|
## Wave 1: Foundations
|
||||||
|
|
||||||
The contract surface in code, engine-blind: everything later waves
|
The contract surface in code, engine-blind: everything later waves
|
||||||
implement *against*, and nothing that needs a live SSH session to
|
implement *against*, and nothing that needs a live SSH session to
|
||||||
test. The alkssh analog of alkgit's seam wave.
|
test. The alkssh analog of alkgit's seam wave.
|
||||||
|
|
||||||
- **Manifest landing** (ADR-002) — the dependency set lands with
|
- **Manifest landing** (ADR-002): the dependency set lands with
|
||||||
implementation start: `russh = "0.65"` with `aws-lc-rs` (no `flate2`,
|
implementation start: `russh = "0.65"` with `aws-lc-rs` (no `flate2`,
|
||||||
no `rsa` — the tree RC-free except russh's own `ssh-key` pin),
|
no `rsa`; the tree RC-free except russh's own `ssh-key` pin),
|
||||||
`alkcall 0.10` (identity/ACL types at the wire layer), `alkvault 0.1`
|
`alkcall 0.10` (identity/ACL types at the wire layer), `alkvault 0.1`
|
||||||
(the custody model), plus the standing toolkit (`tokio`, `thiserror`,
|
(the custody model), plus the standing toolkit (`tokio`, `thiserror`,
|
||||||
`tracing`, `parking_lot`, `zeroize`). `edition = "2024"`,
|
`tracing`, `parking_lot`, `zeroize`). `edition = "2024"`,
|
||||||
`rust-version = "1.89"`. The ADR-003 feature graph is declared from
|
`rust-version = "1.89"`. The ADR-003 feature graph is declared from
|
||||||
the first commit: `session`, `git`, `tunnel`, `sftp`, `default = []`
|
the first commit: `session`, `git`, `tunnel`, `sftp`, `default = []`
|
||||||
— features gate nothing yet (the decline core references them from
|
`default = []`: features gate nothing yet (the decline core references them from
|
||||||
wave 2); the per-feature sibling deps (`alktty` under `session`,
|
wave 2); the per-feature sibling deps (`alktty` under `session`,
|
||||||
`alktunnels` under `tunnel`, `alkgit` under `git`) land with their
|
`alktunnels` under `tunnel`, `alkgit` under `git`) land with their
|
||||||
first consuming wave, not as dead manifest entries. All family deps
|
first consuming wave, not as dead manifest entries. All family deps
|
||||||
pinned by version, never sibling-checkout paths; pins verified
|
pinned by version, never sibling-checkout paths; pins verified
|
||||||
against crates.io before committing.
|
against crates.io before committing.
|
||||||
- **Error model** — the crate's `thiserror` taxonomy, fail-closed with
|
- **Error model**: the crate's `thiserror` taxonomy, fail-closed with
|
||||||
no catch-alls: the key-source failure matrix as distinct variants
|
no catch-alls: the key-source failure matrix as distinct variants
|
||||||
(ADR-005 decision 4 — locked / absent / decryption failure / unknown
|
(ADR-005 decision 4: locked / absent / decryption failure / unknown
|
||||||
`key_version`, plus decode failure; the vault's taxonomy reshaped,
|
`key_version`, plus decode failure; the vault's taxonomy reshaped,
|
||||||
never collapsed). Auth declines are *dispositions* (`Auth::Reject`),
|
never collapsed). Auth declines are *dispositions* (`Auth::Reject`),
|
||||||
not errors; wire decline reasons are wave-2 shaped. What the config
|
not errors; wire decline reasons are wave-2 shaped. What the config
|
||||||
surface needs beyond this lands with it.
|
surface needs beyond this lands with it.
|
||||||
- **Config surface** — the pure configuration types both directions
|
- **Config surface**: the pure configuration types both directions
|
||||||
implement against: the **serve-config** (feature wiring; OQ-08's two
|
implement against: the **serve-config** (feature wiring; OQ-08's two
|
||||||
assembly-supplied fields — shell argv and exec wrapper argv, each
|
assembly-supplied fields: shell argv and exec wrapper argv, each
|
||||||
`Option`, unset → its row declines, never a fallback; the
|
`Option`, unset → its row declines, never a fallback; the
|
||||||
`IdentityProvider` handle; the host-key source handle; the tunnel
|
`IdentityProvider` handle; the host-key source handle; the tunnel
|
||||||
`ChannelClient` handle; the `Limits`), the **dial-config** (address,
|
`ChannelClient` handle; the `Limits`), the **dial-config** (address,
|
||||||
key source, `HostKeyPolicy` handle, keepalive/inactivity/window
|
key source, `HostKeyPolicy` handle, keepalive/inactivity/window
|
||||||
knobs), and **`Limits`** — the bounded-resources budget model
|
knobs), and **`Limits`**, the bounded-resources budget model
|
||||||
(`overview.md`'s table: in-crate max-concurrent-channels cap,
|
(`overview.md`'s table: in-crate max-concurrent-channels cap,
|
||||||
exec-parse length caps, and the russh knob mapping with the
|
exec-parse length caps, and the russh knob mapping with the
|
||||||
documented default postures — `max_auth_attempts` 10 sized for the
|
documented default postures: `max_auth_attempts` 10 sized for the
|
||||||
counting `none`-probe, keepalives 15 s / 3 client-side per the
|
counting `none`-probe, keepalives 15 s / 3 client-side per the
|
||||||
reverse-runner evidence).
|
reverse-runner evidence).
|
||||||
- **Vault-backed key sources** (ADR-005) — the custody model in code:
|
- **Vault-backed key sources** (ADR-005): the custody model in code:
|
||||||
decrypt-at-use (`vault.decrypt` → `Zeroizing<String>` wrap at the
|
decrypt-at-use (`vault.decrypt` → `Zeroizing<String>` wrap at the
|
||||||
adapter seam → one-time `decode_secret_key(plain, None)`), the
|
adapter seam → one-time `decode_secret_key(plain, None)`), the
|
||||||
client dial-key source and the server host-key load path
|
client dial-key source and the server host-key load path
|
||||||
(decrypt-at-construction; locked vault → no server constructed),
|
(decrypt-at-construction; locked vault → no server constructed),
|
||||||
`EncryptedData` opaque to the crate, plaintext never persists, every
|
`EncryptedData` opaque to the crate, plaintext never persists, every
|
||||||
failure mode mapped to its distinct error variant.
|
failure mode mapped to its distinct error variant.
|
||||||
- **Host-key policy seam** (OQ-05) — the `HostKeyPolicy` trait the
|
- **Host-key policy seam** (OQ-05): the `HostKeyPolicy` trait the
|
||||||
client dial calls with (host, port, presented fingerprint): case-1
|
client dial calls with (host, port, presented fingerprint): case-1
|
||||||
pins (hard-decline on mismatch) and case-2 assembly policies are the
|
pins (hard-decline on mismatch) and case-2 assembly policies are the
|
||||||
implementor's; the crate ships the fail-closed default (reject
|
implementor's; the crate ships the fail-closed default (reject
|
||||||
unknown) and is otherwise policy-blind — no accept-all built-in.
|
unknown) and is otherwise policy-blind; no accept-all built-in.
|
||||||
- **Fingerprint** (ADR-004) — the `SHA256:` + unpadded base64
|
- **Fingerprint** (ADR-004): the `SHA256:` + unpadded base64
|
||||||
computation via the `ssh-key` re-export (`HashAlg::Sha256`); the
|
computation via the `ssh-key` re-export (`HashAlg::Sha256`); the
|
||||||
string format is a contract invariant, proven against independent
|
string format is a contract invariant, proven against independent
|
||||||
issuers (`ssh-keygen -lf` vectors, the POC-1 §1 checkpoint shape).
|
issuers (`ssh-keygen -lf` vectors, the POC-1 §1 checkpoint shape).
|
||||||
- **Test doubles** — the in-memory seams every later wave's contract
|
- **Test doubles**: the in-memory seams every later wave's contract
|
||||||
tests run against: an in-memory `IdentityProvider` (registered
|
tests run against: an in-memory `IdentityProvider` (registered
|
||||||
fingerprints → identities), an in-memory vault (encrypt/decrypt
|
fingerprints → identities), an in-memory vault (encrypt/decrypt
|
||||||
round-trip + the failure postures), an in-memory `HostKeyPolicy`,
|
round-trip + the failure postures), an in-memory `HostKeyPolicy`,
|
||||||
@@ -123,10 +123,10 @@ across both directions. The gate reads the landed surface against
|
|||||||
ADR-002/003/004/005 and the OQ-08/05 resolutions line by line.
|
ADR-002/003/004/005 and the OQ-08/05 resolutions line by line.
|
||||||
|
|
||||||
**Decomposition record** (2026-10-28): wave 1 decomposed into eight
|
**Decomposition record** (2026-10-28): wave 1 decomposed into eight
|
||||||
task files under `tasks/implementation/` (flat — one track, no
|
task files under `tasks/implementation/` (flat, one track, no
|
||||||
per-track subdir). Shape: `w1-manifest` first (the ADR-002 dependency
|
per-track subdir). Shape: `w1-manifest` first (the ADR-002 dependency
|
||||||
landing + the ADR-003 feature graph declaration everything lands
|
landing + the ADR-003 feature graph declaration everything lands
|
||||||
inside); then four independent foundations in parallel —
|
inside); then four independent foundations in parallel:
|
||||||
`w1-error-model` (the `KeySourceError` five-variant taxonomy),
|
`w1-error-model` (the `KeySourceError` five-variant taxonomy),
|
||||||
`w1-fingerprint` (the contract-invariant computation + parity
|
`w1-fingerprint` (the contract-invariant computation + parity
|
||||||
vectors), `w1-key-sources` (the ADR-005 custody chain, over manifest +
|
vectors), `w1-key-sources` (the ADR-005 custody chain, over manifest +
|
||||||
@@ -140,7 +140,7 @@ gate) depending on all seven.
|
|||||||
Interpretations recorded (flagged for the wave-1 gate):
|
Interpretations recorded (flagged for the wave-1 gate):
|
||||||
|
|
||||||
- **Per-feature sibling deps land with their first consuming wave**,
|
- **Per-feature sibling deps land with their first consuming wave**,
|
||||||
not in the wave-1 manifest — `alktty`/`alktunnels`/`alkgit` would be
|
not in the wave-1 manifest: `alktty`/`alktunnels`/`alkgit` would be
|
||||||
dead manifest entries in a build where the features gate nothing
|
dead manifest entries in a build where the features gate nothing
|
||||||
yet; the wave-1 manifest lands russh + alkcall + alkvault + the
|
yet; the wave-1 manifest lands russh + alkcall + alkvault + the
|
||||||
toolkit only. The feature *names* are declared from the first
|
toolkit only. The feature *names* are declared from the first
|
||||||
@@ -152,25 +152,25 @@ Interpretations recorded (flagged for the wave-1 gate):
|
|||||||
shaped.
|
shaped.
|
||||||
- **The policy verdict type** (bool vs a richer reason-carrying
|
- **The policy verdict type** (bool vs a richer reason-carrying
|
||||||
verdict) is pinned in `w1-host-key-policy` against the russh
|
verdict) is pinned in `w1-host-key-policy` against the russh
|
||||||
`check_server_key` hook shape at this pin — flagged for the gate.
|
`check_server_key` hook shape at this pin; flagged for the gate.
|
||||||
- **The vault-double shape** (wrap alkvault's in-memory construction
|
- **The vault-double shape** (wrap alkvault's in-memory construction
|
||||||
if it ships one, else a local double behind the source trait) is
|
if it ships one, else a local double behind the source trait) is
|
||||||
pinned in `w1-test-doubles` — flagged for the gate.
|
pinned in `w1-test-doubles`; flagged for the gate.
|
||||||
|
|
||||||
## Wave 2 — The server stack
|
## Wave 2: The server stack
|
||||||
|
|
||||||
The russh server substrate every mapping composes into — the door
|
The russh server substrate every mapping composes into (the door
|
||||||
frame, before any door is hung:
|
frame, before any door is hung):
|
||||||
|
|
||||||
- **`SshAdapter::handle` + `Config` construction** (ADR-001/002) — the
|
- **`SshAdapter::handle` + `Config` construction** (ADR-001/002): the
|
||||||
mount point ADR-001 pins as the `alk/ssh` receiving endpoint: a
|
mount point ADR-001 pins as the `alk/ssh` receiving endpoint: a
|
||||||
generic stream in, the full russh server stack per stream (edge
|
generic stream in, the full russh server stack per stream (edge
|
||||||
termination only — no ALPN code, no relay leg; the pin is a ledger
|
termination only: no ALPN code, no relay leg; the pin is a ledger
|
||||||
entry, already decided). `Config` built from the serve-config:
|
entry, already decided). `Config` built from the serve-config:
|
||||||
honest advertisement (`methods = {publickey}` exactly, ADR-004
|
honest advertisement (`methods = {publickey}` exactly, ADR-004
|
||||||
decision 4), host keys from the vault-backed source (locked → no
|
decision 4), host keys from the vault-backed source (locked → no
|
||||||
server constructed), the bounded knobs from `Limits`.
|
server constructed), the bounded knobs from `Limits`.
|
||||||
- **The auth bridge** (ADR-004) — the dispositional handler surface:
|
- **The auth bridge** (ADR-004): the dispositional handler surface:
|
||||||
`auth_publickey` fingerprint → `resolve_from_fingerprint` → accept
|
`auth_publickey` fingerprint → `resolve_from_fingerprint` → accept
|
||||||
with the resolved identity / reject fail-closed; ed25519-only
|
with the resolved identity / reject fail-closed; ed25519-only
|
||||||
(non-ed25519 keys declined explicitly); `auth_publickey_offered`
|
(non-ed25519 keys declined explicitly); `auth_publickey_offered`
|
||||||
@@ -179,24 +179,24 @@ frame, before any door is hung:
|
|||||||
`auth_openssh_certificate` explicit declines (family contract, not
|
`auth_openssh_certificate` explicit declines (family contract, not
|
||||||
inherited defaults); the username carries no semantics (OQ-03).
|
inherited defaults); the username carries no semantics (OQ-03).
|
||||||
- **Parse-only dispatch + the fail-closed decline core** (ADR-006 as
|
- **Parse-only dispatch + the fail-closed decline core** (ADR-006 as
|
||||||
amended, ADR-003) — session-channel dispatch skeleton (parse path:
|
amended, ADR-003): session-channel dispatch skeleton (parse path:
|
||||||
git-parseable exec → `git`, else → `session`; request-less session
|
git-parseable exec → `git`, else → `session`; request-less session
|
||||||
channels decline), unfeatured channel types decline at open, x11 /
|
channels decline), unfeatured channel types decline at open, x11 /
|
||||||
`auth-agent` always decline (never feature-gated), the sftp
|
`auth-agent` always decline (never feature-gated), the sftp
|
||||||
subsystem decline-with-reason placeholder (OQ-13 — folded here,
|
subsystem decline-with-reason placeholder (OQ-13, folded here
|
||||||
server side), **decline first, alloc never** (the POC-3 stage-A
|
server side), **decline first, alloc never** (the POC-3 stage-A
|
||||||
matrix: unknown repo → collapsed generic denial before any protocol
|
matrix: unknown repo → collapsed generic denial before any protocol
|
||||||
byte).
|
byte).
|
||||||
- **Spawn-and-return plumbing** (ADR-006 decision 4) — the structural
|
- **Spawn-and-return plumbing** (ADR-006 decision 4): the structural
|
||||||
constraint in code: every serving path `tokio::spawn`ed, handlers
|
constraint in code: every serving path `tokio::spawn`ed, handlers
|
||||||
return fast, exit/status reporting from spawned tasks rides the
|
return fast, exit/status reporting from spawned tasks rides the
|
||||||
cloned `Session::handle()`. The reporting *mechanics* land here; the
|
cloned `Session::handle()`. The reporting *mechanics* land here; the
|
||||||
derivation contract's rules are wave-3 session-mapping material.
|
derivation contract's rules are wave-3 session-mapping material.
|
||||||
- **Bounded resources** — the in-crate concurrent-channel cap enforced
|
- **Bounded resources**: the in-crate concurrent-channel cap enforced
|
||||||
at the handler surface, `inactivity_timeout`/`max_auth_attempts`
|
at the handler surface, `inactivity_timeout`/`max_auth_attempts`
|
||||||
wired from `Limits`, per-length exec-parse limits reserved for the
|
wired from `Limits`, per-length exec-parse limits reserved for the
|
||||||
grammar's consumption (wave 3).
|
grammar's consumption (wave 3).
|
||||||
- **Loopback harness** — the in-process client/server test pair the
|
- **Loopback harness**: the in-process client/server test pair the
|
||||||
decline matrix and every mapping's contract tests run through
|
decline matrix and every mapping's contract tests run through
|
||||||
(in-memory doubles from wave 1; real-tool smokes ride the mapping
|
(in-memory doubles from wave 1; real-tool smokes ride the mapping
|
||||||
waves).
|
waves).
|
||||||
@@ -208,61 +208,62 @@ corrects.
|
|||||||
green): wave 2 decomposed into six task files under
|
green): wave 2 decomposed into six task files under
|
||||||
`tasks/implementation/` plus the gate. Shape: `w2-server-mount` first
|
`tasks/implementation/` plus the gate. Shape: `w2-server-mount` first
|
||||||
(the `SshAdapter` mount + russh `Config` construction from the
|
(the `SshAdapter` mount + russh `Config` construction from the
|
||||||
serve-config — the substrate, with the Handler skeletoned at its
|
serve-config, the substrate, with the Handler skeletoned at its
|
||||||
fail-closed defaults); then `w2-auth-bridge` (the ADR-004
|
fail-closed defaults); then `w2-auth-bridge` (the ADR-004
|
||||||
dispositions fill the auth hooks, over the mount + the wave-1
|
dispositions fill the auth hooks, over the mount + the wave-1
|
||||||
fingerprint path and doubles); then `w2-loopback-harness` (the
|
fingerprint path and doubles); then `w2-loopback-harness` (the
|
||||||
in-process client/server pair — the contract-test vehicle — proving
|
in-process client/server pair, the contract-test vehicle, proving
|
||||||
the doubles live through the stack); then `w2-dispatch-decline-core`
|
the doubles live through the stack); then `w2-dispatch-decline-core`
|
||||||
(parse-only dispatch + the fail-closed decline core, contract-tested
|
(parse-only dispatch + the fail-closed decline core, contract-tested
|
||||||
per feature build through the harness); then `w2-spawn-return` (the
|
per feature build through the harness); then `w2-spawn-return` (the
|
||||||
structural constraint pinned by test + the exit-reporting mechanics
|
structural constraint pinned by test + the exit-reporting mechanics
|
||||||
via the cloned `Session::handle()`); then `w2-bounded-resources` (the
|
via the cloned `Session::handle()`); then `w2-bounded-resources` (the
|
||||||
in-crate channel cap at the handler surface); and `review-wave-2`
|
in-crate channel cap at the handler surface); and `review-wave-2`
|
||||||
(the gate) depending on all six. The chain is deliberately sequential
|
(the gate) depending on all six. The chain is deliberately sequential:
|
||||||
— the auth hooks, the dispatch, and the cap all edit the same
|
the auth hooks, the dispatch, and the cap all edit the same
|
||||||
per-connection Handler impl, so no two tasks hold it in parallel.
|
per-connection Handler impl, so no two tasks hold it in parallel.
|
||||||
|
|
||||||
Interpretations recorded (flagged for the wave-2 gate):
|
Interpretations recorded (flagged for the wave-2 gate):
|
||||||
|
|
||||||
- **The git-parseability probe in wave 2 is skeleton-level** (service
|
- **The git-parseability probe in wave 2 is skeleton-level** (service
|
||||||
prefix + path shape) — enough to contract-test the routing decision
|
prefix + path shape), which is enough to contract-test the routing
|
||||||
|
decision
|
||||||
and the session-build's git-gated decline; the full grammar
|
and the session-build's git-gated decline; the full grammar
|
||||||
(quoted/unquoted paths, service-only → decline-with-reason,
|
(quoted/unquoted paths, service-only → decline-with-reason,
|
||||||
per-length limits) is wave-3's git task, which replaces the probe.
|
per-length limits) is wave-3's git task, which replaces the probe.
|
||||||
- **Wave-2 routing stubs decline cleanly at the family-seam boundary**
|
- **Wave-2 routing stubs decline cleanly at the family-seam boundary**:
|
||||||
— the seams land in wave 3; the stub behavior is documented as
|
the seams land in wave 3; the stub behavior is documented as
|
||||||
skeleton so the routing decisions are contract-tested now. The
|
skeleton so the routing decisions are contract-tested now. The
|
||||||
spawn-and-return mechanics are proven on a stub serving path.
|
spawn-and-return mechanics are proven on a stub serving path.
|
||||||
- **The adapter error surface is shaped in `w2-server-mount`** — the
|
- **The adapter error surface is shaped in `w2-server-mount`**: the
|
||||||
construction error wrapping `KeySourceError` (locked → no server)
|
construction error wrapping `KeySourceError` (locked → no server)
|
||||||
plus the Handler `type Error` (`From<russh::Error>`); this is where
|
plus the Handler `type Error` (`From<russh::Error>`); this is where
|
||||||
the wave-1 note's "wire decline reasons are wave-2 shaped" lands.
|
the wave-1 note's "wire decline reasons are wave-2 shaped" lands.
|
||||||
- **The harness transport and placement are pinned in
|
- **The harness transport and placement are pinned in
|
||||||
`w2-loopback-harness`** (duplex pair vs TCP loopback; module
|
`w2-loopback-harness`** (duplex pair vs TCP loopback; module
|
||||||
placement beside the wave-1 doubles) — flagged for the gate.
|
placement beside the wave-1 doubles); flagged for the gate.
|
||||||
|
|
||||||
## Wave 3 — Server mappings
|
## Wave 3: Server mappings
|
||||||
|
|
||||||
Three parallel branches over the stack (plus the folded sftp row),
|
Three parallel branches over the stack (plus the folded sftp row),
|
||||||
each carrying a minimal real-tool smoke (one clone, one `-L`, one pty
|
each carrying a minimal real-tool smoke (one clone, one `-L`, one pty
|
||||||
session — the POC-3 gates re-landed cheaply); the full matrix is
|
session: the POC-3 gates re-landed cheaply); the full matrix is
|
||||||
wave 5's:
|
wave 5's:
|
||||||
|
|
||||||
- **session → alktty** — the translation bridge, not a byte pump
|
- **session → alktty**: the translation bridge, not a byte pump
|
||||||
(POC-3 §D.1): `pty_request` args → `TerminalParamsWire` (SSH modes →
|
(POC-3 §D.1): `pty_request` args → `TerminalParamsWire` (SSH modes →
|
||||||
the reserved modes JSON); shell argv / exec wrapper from the
|
the reserved modes JSON); shell argv / exec wrapper from the
|
||||||
serve-config (`wrapper ++ [cmd]`, never naively split; unset → clean
|
serve-config (`wrapper ++ [cmd]`, never naively split; unset → clean
|
||||||
decline — OQ-08); stdin/stdout/stderr chunk translation
|
decline, per OQ-08); stdin/stdout/stderr chunk translation
|
||||||
(`extended_data(1)` for stderr); `window_change_request`/`signal`
|
(`extended_data(1)` for stderr); `window_change_request`/`signal`
|
||||||
rows; `env_request` declined unconditionally (OQ-09); the **exit
|
rows; `env_request` declined unconditionally (OQ-09); the **exit
|
||||||
derivation contract** (`mapping.md` rules 1–6: `code ≥ 0` →
|
derivation contract** (`mapping.md` rules 1–6: `code ≥ 0` →
|
||||||
`exit-status(N)`, negative `−N` → `exit-signal(name)` via the
|
`exit-status(N)`, negative `−N` → `exit-signal(name)` via the
|
||||||
server-side Unix signal-number→name table, `−1` → `exit-status(255)`
|
server-side Unix signal-number→name table, `−1` → `exit-status(255)`
|
||||||
never a signal claim, `core_dumped` never asserted, PTY-path rule-5
|
never a signal claim, `core_dumped` never asserted, PTY-path rule-5
|
||||||
fidelity limit) — exit-status sent before close, from the spawned
|
fidelity limit); exit-status sent before close, from the spawned
|
||||||
task.
|
task.
|
||||||
- **git → alkgit** — exec-only (ADR-006 decision 2): the parse grammar
|
- **git → alkgit**: exec-only (ADR-006 decision 2), with the parse grammar
|
||||||
(`git-{upload,receive}-pack '/repo'`, quoted/unquoted paths,
|
(`git-{upload,receive}-pack '/repo'`, quoted/unquoted paths,
|
||||||
service-only → decline-with-reason, non-git → parse-then-fail,
|
service-only → decline-with-reason, non-git → parse-then-fail,
|
||||||
per-length limits), unknown-repo → collapsed generic denial before
|
per-length limits), unknown-repo → collapsed generic denial before
|
||||||
@@ -270,10 +271,10 @@ wave 5's:
|
|||||||
`serve_duplex_resolved(repo_id, identity, service, duplex, limits,
|
`serve_duplex_resolved(repo_id, identity, service, duplex, limits,
|
||||||
…)` over the channel duplex, process-mode dialect, exit-status both
|
…)` over the channel duplex, process-mode dialect, exit-status both
|
||||||
directions.
|
directions.
|
||||||
- **tunnel → alktunnels route 1** (ADR-006 decision 1) — the
|
- **tunnel → alktunnels route 1** (ADR-006 decision 1): the
|
||||||
adapter-side bridge: the serve-config's `ChannelClient` into the
|
adapter-side bridge taking the serve-config's `ChannelClient` into
|
||||||
family fabric (identity propagated once via `set_identity` so
|
the family fabric (identity propagated once via `set_identity` so
|
||||||
`AccessControl::check` gates the mapped opens — the visible-surface
|
`AccessControl::check` gates the mapped opens, the visible-surface
|
||||||
invariant), `TunnelSession::open_direct` + `take_halves()` pumped
|
invariant), `TunnelSession::open_direct` + `take_halves()` pumped
|
||||||
against the russh channel, accept only after the fabric open
|
against the russh channel, accept only after the fabric open
|
||||||
succeeds (failure → explicit `ConnectFailed` reject),
|
succeeds (failure → explicit `ConnectFailed` reject),
|
||||||
@@ -281,7 +282,7 @@ wave 5's:
|
|||||||
clean open-decline off-Unix), the `forwarded-tcpip` handler shape,
|
clean open-decline off-Unix), the `forwarded-tcpip` handler shape,
|
||||||
the `-R` listen flow (`tcpip_forward`/`streamlocal_forward` + the
|
the `-R` listen flow (`tcpip_forward`/`streamlocal_forward` + the
|
||||||
same runtime-Unix gate).
|
same runtime-Unix gate).
|
||||||
- **sftp placeholder** — the decline-with-reason rows (folded into the
|
- **sftp placeholder**: the decline-with-reason rows (folded into the
|
||||||
dispatch task server-side; the client-side placeholder folds into
|
dispatch task server-side; the client-side placeholder folds into
|
||||||
wave 4's acceptor/op-row work; the wave-5 matrix covers both
|
wave 4's acceptor/op-row work; the wave-5 matrix covers both
|
||||||
explicitly).
|
explicitly).
|
||||||
@@ -293,74 +294,73 @@ corrects.
|
|||||||
green): wave 3 decomposed into six task files under
|
green): wave 3 decomposed into six task files under
|
||||||
`tasks/implementation/` plus the gate. Shape: `w3-seam-config` first
|
`tasks/implementation/` plus the gate. Shape: `w3-seam-config` first
|
||||||
(the serve-config's per-feature seam handles + the optional
|
(the serve-config's per-feature seam handles + the optional
|
||||||
`alktty`/`alkgit` deps — see the interpretations — the mappings
|
`alktty`/`alkgit` deps; see the interpretations; the mappings
|
||||||
dispatch against them, so it lands before the mappings); then
|
dispatch against them, so it lands before the mappings); then
|
||||||
`w3-decline-tracing` (the wave-2 gate's booked finding: the decline/
|
`w3-decline-tracing` (the wave-2 gate's booked finding: the decline/
|
||||||
serve-path event vocabulary, landed **before** the mappings so they
|
serve-path event vocabulary, landed **before** the mappings so they
|
||||||
inherit a uniform shape); then the three mapping branches
|
inherit a uniform shape); then the three mapping branches
|
||||||
deliberately sequential — `w3-git-mapping` (the full grammar replaces
|
(deliberately sequential): `w3-git-mapping` (the full grammar replaces
|
||||||
the wave-2 probe + the `serve_duplex_resolved` dispatch + the
|
the wave-2 probe + the `serve_duplex_resolved` dispatch + the
|
||||||
store-and-take handoff it lands first for the session task to reuse),
|
store-and-take handoff it lands first for the session task to reuse),
|
||||||
`w3-tty-session` (the translation bridge + the exit-derivation
|
`w3-tty-session` (the translation bridge + the exit-derivation
|
||||||
contract — the biggest single-row surface), `w3-tunnel-direct` (the
|
contract, the biggest single-row surface), `w3-tunnel-direct` (the
|
||||||
POC-proven route-1 bridge leg + identity `set_identity` propagation),
|
POC-proven route-1 bridge leg + identity `set_identity` propagation),
|
||||||
and `w3-tunnel-listen` (the `-R` listen flow + `forwarded-tcpip`
|
and `w3-tunnel-listen` (the `-R` listen flow + `forwarded-tcpip`
|
||||||
data leg — the POC-untested leg, sized with the stop-and-book
|
data leg: the POC-untested leg, sized with the stop-and-book
|
||||||
provision); and `review-wave-3` (the gate) depending on all six. As
|
provision); and `review-wave-3` (the gate) depending on all six. As
|
||||||
with wave 2, the branch DAG is sequential: the three mappings replace
|
with wave 2, the branch DAG is sequential: the three mappings replace
|
||||||
stubs in the same `Handler` impl and share the decline-matrix test
|
stubs in the same `Handler` impl and share the decline-matrix test
|
||||||
binaries, so no two tasks hold them in parallel — the branches are
|
binaries, so no two tasks hold them in parallel: the branches are
|
||||||
parallel *conceptually* (independent seams), sequential
|
parallel *conceptually* (independent seams), sequential
|
||||||
*logistically*.
|
*logistically*.
|
||||||
|
|
||||||
Interpretations recorded (flagged for the wave-3 gate):
|
Interpretations recorded (flagged for the wave-3 gate):
|
||||||
|
|
||||||
- **The serve-config extension is wave 3's first course correction to
|
- **The serve-config extension is wave 3's first course correction to
|
||||||
the wave-1 config surface** — the assembly-supplied alktty/alkgit
|
the wave-1 config surface**: the assembly-supplied alktty/alkgit
|
||||||
seam handles the mappings' seam signatures take (the tty backends
|
seam handles the mappings' seam signatures take (the tty backends
|
||||||
map + selector + ownership provider; the alkgit backend quartet +
|
map + selector + ownership provider; the alkgit backend quartet +
|
||||||
pipeline semaphore + push-options flag) were never carried by the
|
pipeline semaphore + push-options flag) were never carried by the
|
||||||
wave-1 config bullet, and `w3-seam-config` lands them feature-gated
|
wave-1 config bullet, and `w3-seam-config` lands them feature-gated
|
||||||
with the optional sibling deps (`alktty = "0.8.1"`, `alkgit =
|
with the optional sibling deps (`alktty = "0.8.1"`, `alkgit =
|
||||||
"0.1.0"` — crates.io-verified at decomposition) before the
|
"0.1.0"`, crates.io-verified at decomposition) before the
|
||||||
mappings; `alktunnels = "0.4.1"` still lands with its first
|
mappings; `alktunnels = "0.4.1"` still lands with its first
|
||||||
consuming code (`w3-tunnel-direct`) to avoid a dead entry. The
|
consuming code (`w3-tunnel-direct`) to avoid a dead entry. The
|
||||||
unset-declines posture extends to the new handles (unset → the
|
unset-declines posture extends to the new handles (unset → the
|
||||||
owned rows decline even with the feature on).
|
owned rows decline even with the feature on).
|
||||||
- **The server-side sftp bullet is already discharged** — the OQ-13
|
- **The server-side sftp bullet is already discharged**: the OQ-13
|
||||||
decline rows landed with `w2-dispatch-decline-core` (every subsystem
|
decline rows landed with `w2-dispatch-decline-core` (every subsystem
|
||||||
name declines in every build) and are already contract-tested per
|
name declines in every build) and are already contract-tested per
|
||||||
build in `tests/dispatch_core.rs`; no wave-3 task exists for it —
|
build in `tests/dispatch_core.rs`; no wave-3 task exists for it: the
|
||||||
the client-side placeholder folds into wave 4's acceptor/op-row
|
client-side placeholder folds into wave 4's acceptor/op-row
|
||||||
work and the wave-5 matrix covers both rows explicitly.
|
work and the wave-5 matrix covers both rows explicitly.
|
||||||
- **The `-R` listen flow carries the wave-3 gate's biggest open** —
|
- **The `-R` listen flow carries the wave-3 gate's biggest open**: the fabric-side inbound-forwarded mechanics (who dials the
|
||||||
the fabric-side inbound-forwarded mechanics (who dials the
|
|
||||||
forwarded op at the door, whether the assembly-supplied
|
forwarded op at the door, whether the assembly-supplied
|
||||||
`Arc<ChannelClient>` composition can carry the door-side accept or
|
`Arc<ChannelClient>` composition can carry the door-side accept or
|
||||||
the serve-config's tunnel handle must grow) is decided in
|
the serve-config's tunnel handle must grow) is decided in
|
||||||
`w3-tunnel-listen` against alkcall 0.10.1 / alktunnels 0.4.1's
|
`w3-tunnel-listen` against alkcall 0.10.1 / alktunnels 0.4.1's
|
||||||
published surfaces; if the topology does not close, the task stops
|
published surfaces; if the topology does not close, the task stops
|
||||||
and books it for the gate — nothing is silently forked or absorbed.
|
and books it for the gate; nothing is silently forked or absorbed.
|
||||||
- **The wave-2 gate's three booked items all have wave-3 homes** — the
|
- **The wave-2 gate's three booked items all have wave-3 homes**: the
|
||||||
decline-path tracing task (`w3-decline-tracing`), the forwarded-
|
decline-path tracing task (`w3-decline-tracing`), the forwarded-
|
||||||
tcpip acceptor vocabulary (`w3-tunnel-listen`, with the client-side
|
tcpip acceptor vocabulary (`w3-tunnel-listen`, with the client-side
|
||||||
accept hook landing in the harness), and the deferred identity-
|
accept hook landing in the harness), and the deferred identity-
|
||||||
landing assertion (each mapping asserts the resolved identity
|
landing assertion (each mapping asserts the resolved identity
|
||||||
reaches its seam; the tunnel branch additionally propagates it via
|
reaches its seam; the tunnel branch additionally propagates it via
|
||||||
`set_identity` — auth.md §Server direction step 4).
|
`set_identity`; see auth.md §Server direction step 4).
|
||||||
|
|
||||||
## Wave 4 — Client direction
|
## Wave 4: Client direction
|
||||||
|
|
||||||
The door outward (independent of wave 3 — shares only wave 1's
|
The door outward (independent of wave 3; shares only wave 1's
|
||||||
foundations), each branch carrying its real-tool smoke where cheap:
|
foundations), each branch carrying its real-tool smoke where cheap:
|
||||||
|
|
||||||
- **Dial surface** (`client.md`) — connect with address, key source
|
- **Dial surface** (`client.md`): connect with address, key source
|
||||||
(decrypt-at-dial, fail-closed on locked/absent — never env vars),
|
(decrypt-at-dial, fail-closed on locked/absent; never env vars),
|
||||||
host-key policy at `check_server_key` (the wave-1 seam consumed:
|
host-key policy at `check_server_key` (the wave-1 seam consumed:
|
||||||
case-1 pins hard-decline on mismatch, case-2 policies decide), the
|
case-1 pins hard-decline on mismatch, case-2 policies decide), the
|
||||||
bounded config (keepalives 15 s / 3, inactivity, window knobs). The
|
bounded config (keepalives 15 s / 3, inactivity, window knobs). The
|
||||||
authenticated connection handle everything else rides.
|
authenticated connection handle everything else rides.
|
||||||
- **Op rows** — family op → vanilla translation per `client.md`'s
|
- **Op rows**: family op → vanilla translation per `client.md`'s
|
||||||
table: session exec/pty opens (the POC-3 §D.3 mechanics:
|
table: session exec/pty opens (the POC-3 §D.3 mechanics:
|
||||||
`Channel::exec()` not `request_exec`, Success/Failure via
|
`Channel::exec()` not `request_exec`, Success/Failure via
|
||||||
`wait()`-drain not `Err`, `window_change`/`signal` before
|
`wait()`-drain not `Err`, `window_change`/`signal` before
|
||||||
@@ -369,14 +369,14 @@ foundations), each branch carrying its real-tool smoke where cheap:
|
|||||||
connection stays usable); git exec consumption (the same grammar the
|
connection stays usable); git exec consumption (the same grammar the
|
||||||
server parses, pointed upstream); the sftp subsystem placeholder
|
server parses, pointed upstream); the sftp subsystem placeholder
|
||||||
decline.
|
decline.
|
||||||
- **The `-R` leg** — `tcpip_forward(address, port) -> bound` (the
|
- **The `-R` leg**: `tcpip_forward(address, port) -> bound` (the
|
||||||
actually-bound port returned), server-initiated `forwarded-tcpip`
|
actually-bound port returned), server-initiated `forwarded-tcpip`
|
||||||
opens routed to the **acceptor registry**: served only under a
|
opens routed to the **acceptor registry**: served only under a
|
||||||
registered feature's acceptor, unregistered/unfeatured accepted
|
registered feature's acceptor, unregistered/unfeatured accepted
|
||||||
opens declined cleanly (never russh's accept-and-drop default),
|
opens declined cleanly (never russh's accept-and-drop default),
|
||||||
late-install across a live connection (the alkminer dynamic),
|
late-install across a live connection (the alkminer dynamic),
|
||||||
`streamlocal_forward` with the runtime-Unix gate.
|
`streamlocal_forward` with the runtime-Unix gate.
|
||||||
- **Lifecycle primitives** (OQ-12) — explicitly cancellable
|
- **Lifecycle primitives** (OQ-12): explicitly cancellable
|
||||||
listener/session handles, clean `Disconnect::ByApplication`; **no
|
listener/session handles, clean `Disconnect::ByApplication`; **no
|
||||||
reconnect or restore policy owned by the crate** (assembly policy;
|
reconnect or restore policy owned by the crate** (assembly policy;
|
||||||
the primitives make it possible).
|
the primitives make it possible).
|
||||||
@@ -390,24 +390,24 @@ the mappings surfaced).
|
|||||||
green): wave 4 decomposed into eight task files under
|
green): wave 4 decomposed into eight task files under
|
||||||
`tasks/implementation/` plus the gate. The wave-3 gate's four booked
|
`tasks/implementation/` plus the gate. The wave-3 gate's four booked
|
||||||
findings are folded in as dedicated tasks (the gate's own
|
findings are folded in as dedicated tasks (the gate's own
|
||||||
disposition — "wave-4 material"), not wave-3 backports: the
|
disposition, "wave-4 material"), not wave-3 backports. The
|
||||||
hardening set is server-side and independent of the client chain —
|
hardening set is server-side and independent of the client chain:
|
||||||
`w4-git-exit-close` (booked item 1: the git row's status-before-close
|
`w4-git-exit-close` (booked item 1: the git row's status-before-close
|
||||||
by construction — the seam/shape decided + recorded in-task),
|
by construction, the seam/shape decided + recorded in-task),
|
||||||
`w4-channel-not-held-rows` (item 2: the guard's wire rows on all
|
`w4-channel-not-held-rows` (item 2: the guard's wire rows on all
|
||||||
three rows, per build — test-only), `w4-tty-coverage` (item 3: the
|
three rows, per build, test-only), `w4-tty-coverage` (item 3: the
|
||||||
`channel_close`-fired cancel path + the Closed/Break no-exit-chunk
|
`channel_close`-fired cancel path + the Closed/Break no-exit-chunk
|
||||||
rows + the write_all no-deadline known-limit record), and
|
rows + the write_all no-deadline known-limit record), and
|
||||||
`w4-router-collision` (item 4: the `ListenRouter` token-collision
|
`w4-router-collision` (item 4: the `ListenRouter` token-collision
|
||||||
fail-closed pre-check); the gate's item 5 (the upstream asks) stays
|
fail-closed pre-check); the gate's item 5 (the upstream asks) stays
|
||||||
booked with its owners — no task, nothing absorbed. The client chain
|
booked with its owners: no task, nothing absorbed. The client chain
|
||||||
is deliberately sequential — `w4-dial` first (the russh client stack
|
is deliberately sequential: `w4-dial` first (the russh client stack
|
||||||
+ the connection handle + the client Handler skeleton at fail-closed
|
+ the connection handle + the client Handler skeleton at fail-closed
|
||||||
defaults, over an in-process fake-`sshd` server double this task
|
defaults, over an in-process fake-`sshd` server double this task
|
||||||
lands), then `w4-op-rows` (exec/pty, direct-tcpip, git exec over the
|
lands), then `w4-op-rows` (exec/pty, direct-tcpip, git exec over the
|
||||||
shared grammar, the sftp client-side decline — the POC-3 §D.3
|
shared grammar, the sftp client-side decline with the POC-3 §D.3
|
||||||
mechanics pinned), then `w4-reverse-listen` (the `-R` leg: forward
|
mechanics pinned), then `w4-reverse-listen` (the `-R` leg: forward
|
||||||
requests + the acceptor registry + late-install — the wave's
|
requests + the acceptor registry + late-install, making it the wave's
|
||||||
highest-risk task, growing the harness's `ForwardedAcceptor`
|
highest-risk task, growing the harness's `ForwardedAcceptor`
|
||||||
vocabulary into the crate's real surface), then `w4-lifecycle` (the
|
vocabulary into the crate's real surface), then `w4-lifecycle` (the
|
||||||
OQ-12 primitives); `review-wave-4` (the gate) depends on all eight.
|
OQ-12 primitives); `review-wave-4` (the gate) depends on all eight.
|
||||||
@@ -420,75 +420,75 @@ are scheduled first as cheap remediation.
|
|||||||
Interpretations recorded (flagged for the wave-4 gate):
|
Interpretations recorded (flagged for the wave-4 gate):
|
||||||
|
|
||||||
- **The client Handler skeleton lands fail-closed in `w4-dial`, the
|
- **The client Handler skeleton lands fail-closed in `w4-dial`, the
|
||||||
acceptor registry in `w4-reverse-listen`** — the dial task pins the
|
acceptor registry in `w4-reverse-listen`**: the dial task pins the
|
||||||
decline-all default for server-initiated opens (the ADR-003 §4
|
decline-all default for server-initiated opens (the ADR-003 §4
|
||||||
rule) and the listen task grows the registry that serves them; the
|
rule) and the listen task grows the registry that serves them; the
|
||||||
split mirrors wave 2's mount-before-mappings shape and keeps the
|
split mirrors wave 2's mount-before-mappings shape and keeps the
|
||||||
dial task's surface reviewable.
|
dial task's surface reviewable.
|
||||||
- **The client-direction test vehicle is an in-process fake `sshd`**
|
- **The client-direction test vehicle is an in-process fake `sshd`**
|
||||||
(a hand-rolled russh server double in `tests/common`) — the
|
(a hand-rolled russh server double in `tests/common`): the
|
||||||
loopback harness drives the *server* direction and is not the
|
loopback harness drives the *server* direction and is not the
|
||||||
vehicle here; real-`sshd` client-direction e2e stays wave-5
|
vehicle here; real-`sshd` client-direction e2e stays wave-5
|
||||||
territory (the plan's wave-5 bullet), with per-task check-gated
|
territory (the plan's wave-5 bullet), with per-task check-gated
|
||||||
real-tool smokes where cheap (the wave-3 smoke posture).
|
real-tool smokes where cheap (the wave-3 smoke posture).
|
||||||
- **No `DialConfig` course-correction is anticipated** — the wave-1
|
- **No `DialConfig` course-correction is anticipated**: the wave-1
|
||||||
shape already carries everything `client.md`'s dial surface names;
|
shape already carries everything `client.md`'s dial surface names;
|
||||||
`w4-dial` decides + records if that proves wrong (the
|
`w4-dial` decides + records if that proves wrong (the
|
||||||
`w3-seam-config` pattern), and the acceptor registry is
|
`w3-seam-config` pattern), and the acceptor registry is
|
||||||
client-connection state, not serve-config.
|
client-connection state, not serve-config.
|
||||||
- **The hardening tasks are ordered by single-writer needs, not
|
- **The hardening tasks are ordered by single-writer needs, not
|
||||||
risk** — `w4-channel-not-held-rows` follows `w4-git-exit-close`
|
risk**: `w4-channel-not-held-rows` follows `w4-git-exit-close`
|
||||||
(both touch `tests/git_mapping.rs`) and `w4-tty-coverage` follows
|
(both touch `tests/git_mapping.rs`) and `w4-tty-coverage` follows
|
||||||
the rows task (both touch `tests/tty_mapping.rs`), the wave-3
|
the rows task (both touch `tests/tty_mapping.rs`), the wave-3
|
||||||
no-two-tasks-hold-one-file lesson applied to the test binaries.
|
no-two-tasks-hold-one-file lesson applied to the test binaries.
|
||||||
|
|
||||||
## Wave 5 — Validation & release
|
## Wave 5: Validation & release
|
||||||
|
|
||||||
- **The decline-matrix contract suite** (`mapping.md`'s table) — every
|
- **The decline-matrix contract suite** (`mapping.md`'s table): every
|
||||||
build (none / `git` / `session` / `tunnel` / `--all-features`) ×
|
build (none / `git` / `session` / `tunnel` / `--all-features`) ×
|
||||||
every row it can appear in × both directions, including the sftp
|
every row it can appear in × both directions, including the sftp
|
||||||
placeholder rows and the never-features (x11, `auth-agent`). The
|
placeholder rows and the never-features (x11, `auth-agent`). The
|
||||||
fail-closed invariant is contract territory on both sides of every
|
fail-closed invariant is contract territory on both sides of every
|
||||||
feature (AGENTS.md conventions 5–6).
|
feature (AGENTS.md conventions 5–6).
|
||||||
- **Real-tool e2e** — the POC-3 gates re-landed against the shipped
|
- **Real-tool e2e**: the POC-3 gates re-landed against the shipped
|
||||||
crate: OpenSSH (pty/shell/exec, `-L`, `-R`, `-D`'s `direct-tcpip`
|
crate: OpenSSH (pty/shell/exec, `-L`, `-R`, `-D`'s `direct-tcpip`
|
||||||
leg, decline observability), git 2.43+ (clone/push through the git
|
leg, decline observability), git 2.43+ (clone/push through the git
|
||||||
mapping), curl through a `-D` proxy; client direction against a real
|
mapping), curl through a `-D` proxy; client direction against a real
|
||||||
`sshd` (the reverse-runner shape: exec, direct-tcpip dial, `-R`
|
`sshd` (the reverse-runner shape: exec, direct-tcpip dial, `-R`
|
||||||
listen + acceptor).
|
listen + acceptor).
|
||||||
- **Fuzzing** — the family standing rule (anything worth fuzzing gets
|
- **Fuzzing**: the family standing rule (anything worth fuzzing gets
|
||||||
fuzzed) on the crate's ingress surfaces: the exec parse grammar, the
|
fuzzed) on the crate's ingress surfaces: the exec parse grammar, the
|
||||||
fingerprint format path, the key-decode path, the TerminalParamsWire
|
fingerprint format path, the key-decode path, the TerminalParamsWire
|
||||||
conversion. The `fuzz/` sidecar convention (detached runner, corpus
|
conversion. The `fuzz/` sidecar convention (detached runner, corpus
|
||||||
gate joining the verification list).
|
gate joining the verification list).
|
||||||
- **Release readiness** — crate docs (public-API rustdoc
|
- **Release readiness**: crate docs (public-API rustdoc
|
||||||
warning-free; docs honesty), the feature matrix (`default`, each
|
warning-free; docs honesty), the feature matrix (`default`, each
|
||||||
feature over it, `--all-features`), CHANGELOG/README, publish
|
feature over it, `--all-features`), CHANGELOG/README, publish
|
||||||
dry-run, then **the publish of `alkssh 0.1`** — the gate review that
|
dry-run, then **the publish of `alkssh 0.1`**, the gate review that
|
||||||
closes the plan.
|
closes the plan.
|
||||||
|
|
||||||
**Decomposition record** (2026-10-30, after the wave-4 gate closed
|
**Decomposition record** (2026-10-30, after the wave-4 gate closed
|
||||||
green — the gate's verdict: "wave 5 decomposition is unblocked and
|
green, with the gate's verdict: "wave 5 decomposition is unblocked and
|
||||||
should absorb nothing beyond its own plan"; the wave-3 gate's item-5
|
should absorb nothing beyond its own plan"; the wave-3 gate's item-5
|
||||||
upstream asks verified still booked with their owners, no task): wave
|
upstream asks verified still booked with their owners, no task): wave
|
||||||
5 decomposed into seven task files under `tasks/implementation/`.
|
5 decomposed into seven task files under `tasks/implementation/`.
|
||||||
Shape: four independent implementation tasks over the landed waves —
|
Shape: four independent implementation tasks over the landed waves:
|
||||||
`w5-decline-matrix` (the coverage audit of `mapping.md`'s matrix
|
`w5-decline-matrix` (the coverage audit of `mapping.md`'s matrix
|
||||||
table cell by cell + the gap rows in the existing per-build binaries
|
table cell by cell + the gap rows in the existing per-build binaries
|
||||||
+ the matrix runner joining the verification list), `w5-e2e-server`
|
+ the matrix runner joining the verification list), `w5-e2e-server`
|
||||||
(OpenSSH pty/shell/exec/`-L`/`-R`/`-D`+curl, decline observability,
|
(OpenSSH pty/shell/exec/`-L`/`-R`/`-D`+curl, decline observability,
|
||||||
git clone **and** push — a dedicated new e2e binary, the wave-3
|
git clone **and** push in a dedicated new e2e binary, the wave-3
|
||||||
smokes re-landed/extended), `w5-e2e-client` (the reverse-runner shape
|
smokes re-landed/extended), `w5-e2e-client` (the reverse-runner shape
|
||||||
against a spawned real `sshd` — the sshd fixture, exec/direct-tcpip/
|
against a spawned real `sshd`: the sshd fixture, exec/direct-tcpip/
|
||||||
`-R`+acceptor, the decline rows), and `w5-fuzz-harness` (the `fuzz/`
|
`-R`+acceptor, the decline rows), and `w5-fuzz-harness` (the `fuzz/`
|
||||||
sidecar per the alkgit convention: four targets — exec_parse,
|
sidecar per the alkgit convention: four targets (exec_parse,
|
||||||
fingerprint, key_decode, terminal_params — the shared-library corpus
|
fingerprint, key_decode, terminal_params), the shared-library corpus
|
||||||
gate joining the verification list, the detached runner, the root
|
gate joining the verification list, the detached runner, the root
|
||||||
workspace `exclude`); then `w5-release-readiness` (rustdoc
|
workspace `exclude`); then `w5-release-readiness` (rustdoc
|
||||||
warning-free × builds, README lands, the CHANGELOG 0.1.0 entry, the
|
warning-free × builds, README lands, the CHANGELOG 0.1.0 entry, the
|
||||||
feature matrix, the publish dry-run + package-policy file-list check,
|
feature matrix, the publish dry-run + package-policy file-list check,
|
||||||
the freeze inventory recorded for the gate) over all four;
|
the freeze inventory recorded for the gate) over all four;
|
||||||
`review-wave-5` (the release gate — a **pure review**) over all five;
|
`review-wave-5` (the release gate, a **pure review**) over all five;
|
||||||
and `w5-publish` (the publish checklist: manifest sanity, the full
|
and `w5-publish` (the publish checklist: manifest sanity, the full
|
||||||
verification set, the dry-run, the crate-qualified `alkssh-v0.1.0`
|
verification set, the dry-run, the crate-qualified `alkssh-v0.1.0`
|
||||||
tag, `cargo publish`, the plan's closure recorded) over the gate.
|
tag, `cargo publish`, the plan's closure recorded) over the gate.
|
||||||
@@ -496,17 +496,17 @@ The four implementation tasks are mutually independent by
|
|||||||
construction (disjoint files: the matrix task owns the existing
|
construction (disjoint files: the matrix task owns the existing
|
||||||
per-build binaries, the e2e tasks own new binaries + their own
|
per-build binaries, the e2e tasks own new binaries + their own
|
||||||
fixture modules, the fuzz task owns `fuzz/` + at most
|
fixture modules, the fuzz task owns `fuzz/` + at most
|
||||||
`#[doc(hidden)]` exposures) — parallel *conceptually*; any src/
|
`#[doc(hidden)]` exposures), parallel *conceptually*; any src/
|
||||||
fix either finds is fixed in-task, with the gate as the merge point.
|
fix either finds is fixed in-task, with the gate as the merge point.
|
||||||
|
|
||||||
Interpretations recorded (flagged for the wave-5 gate):
|
Interpretations recorded (flagged for the wave-5 gate):
|
||||||
|
|
||||||
- **The gate-then-publish split** — the plan's release-readiness
|
- **The gate-then-publish split**: the plan's release-readiness
|
||||||
bullet folds the publish into release readiness, but the
|
bullet folds the publish into release readiness, but the
|
||||||
decomposition follows the alkgit pattern: `review-wave-5` stays a
|
decomposition follows the alkgit pattern: `review-wave-5` stays a
|
||||||
pure review and `w5-publish` is its own task (the publish is a
|
pure review and `w5-publish` is its own task (the publish is a
|
||||||
one-way door; it executes only over a green gate).
|
one-way door; it executes only over a green gate).
|
||||||
- **The e2e vehicle/placement** — dedicated new test binaries
|
- **The e2e vehicle/placement**: dedicated new test binaries
|
||||||
(`tests/e2e_server.rs` / `tests/e2e_client.rs`, reusing
|
(`tests/e2e_server.rs` / `tests/e2e_client.rs`, reusing
|
||||||
`tests/common`) rather than extensions of the wave-3/4 mapping
|
`tests/common`) rather than extensions of the wave-3/4 mapping
|
||||||
binaries, keeping those single-writer for the matrix task; the
|
binaries, keeping those single-writer for the matrix task; the
|
||||||
@@ -519,23 +519,23 @@ Interpretations recorded (flagged for the wave-5 gate):
|
|||||||
fuzz-visible exposures per target) is decided + recorded in
|
fuzz-visible exposures per target) is decided + recorded in
|
||||||
`w5-fuzz-harness` (the alkgit pattern); the sidecar is excluded
|
`w5-fuzz-harness` (the alkgit pattern); the sidecar is excluded
|
||||||
from the root single-member workspace and never a root dep.
|
from the root single-member workspace and never a root dep.
|
||||||
- **The tag convention** — crate-qualified `alkssh-v0.1.0` (the
|
- **The tag convention**: crate-qualified `alkssh-v0.1.0` (the
|
||||||
alkgit convention; a bare `v0.1.0` would be ambiguous across the
|
alkgit convention; a bare `v0.1.0` would be ambiguous across the
|
||||||
family's multi-crate repos). Pinned in `w5-publish`; flagged for
|
family's multi-crate repos). Pinned in `w5-publish`; flagged for
|
||||||
the gate to confirm.
|
the gate to confirm.
|
||||||
- **No version bump** — the manifest already carries 0.1.0; the
|
- **No version bump**: the manifest already carries 0.1.0; the
|
||||||
publish task verifies rather than bumps.
|
publish task verifies rather than bumps.
|
||||||
|
|
||||||
## Plan closure (2026-10-30)
|
## Plan closure (2026-10-30)
|
||||||
|
|
||||||
The plan closed at the publish of **`alkssh 0.1.0`** — the publish
|
The plan closed at the publish of **`alkssh 0.1.0`**: the publish
|
||||||
commit, tagged **`alkssh-v0.1.0`** (the crate-qualified alkgit tag
|
commit, tagged **`alkssh-v0.1.0`** (the crate-qualified alkgit tag
|
||||||
convention) and pushed to `origin/main`. Scope authority discharged:
|
convention) and pushed to `origin/main`. Scope authority discharged:
|
||||||
the complete two-direction door shipped (server stack + all mappings,
|
the complete two-direction door shipped (server stack + all mappings,
|
||||||
client stack + op rows, key sources, host-key policy seam, the
|
client stack + op rows, key sources, host-key policy seam, the
|
||||||
decline matrix contract-tested per build per `scripts/matrix.sh` —
|
decline matrix contract-tested per build per `scripts/matrix.sh`
|
||||||
see AGENTS.md's verification list), the assembly stays out (seams
|
(see AGENTS.md's verification list), the assembly stays out (seams
|
||||||
only — identity/ACL/fabric policy is downstream composition), no
|
only; identity/ACL/fabric policy is downstream composition), no
|
||||||
binary, single crate. All five review gates closed green (waves 1–4
|
binary, single crate. All five review gates closed green (waves 1–4
|
||||||
2026-10-28/29; the wave-5 release gate 2026-10-30).
|
2026-10-28/29; the wave-5 release gate 2026-10-30).
|
||||||
|
|
||||||
@@ -555,9 +555,9 @@ mapping.md's correlation-miss event-vocabulary nuance) and the
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- `docs/architecture/` — overview, mapping, client, auth,
|
- `docs/architecture/`: overview, mapping, client, auth,
|
||||||
open-questions (all reviewed 2026-10-28), ADR-001…006
|
open-questions (all reviewed 2026-10-28), ADR-001…006
|
||||||
- `docs/research/` — vision, russh inventory, auth bridge, mapping
|
- `docs/research/`: vision, russh inventory, auth bridge, mapping
|
||||||
table, POC-1/2/3 findings, reverse-runner extraction
|
table, POC-1/2/3 findings, reverse-runner extraction
|
||||||
- Family ADR-003 (the structural decision); the alkgit implementation
|
- Family ADR-003 (the structural decision); the alkgit implementation
|
||||||
plan (the pattern this follows)
|
plan (the pattern this follows)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Auth bridge — russh auth ↔ alkcall identity
|
# Auth bridge: russh auth ↔ alkcall identity
|
||||||
|
|
||||||
Status: Draft (phase 0 research). Sources: alkcall 0.10.1 source
|
Status: Draft (phase 0 research). Sources: alkcall 0.10.1 source
|
||||||
(`src/core/auth.rs`, `src/core/types.rs`, `src/protocol/dispatch.rs`),
|
(`src/core/auth.rs`, `src/core/types.rs`, `src/protocol/dispatch.rs`),
|
||||||
@@ -31,7 +31,7 @@ pub trait IdentityProvider: Send + Sync + 'static {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`IdentityProvider` is a trait only — implementations are supplied by
|
`IdentityProvider` is a trait only; implementations are supplied by
|
||||||
the assembly layer (alkcall ships `NoopIdentityProvider`, which
|
the assembly layer (alkcall ships `NoopIdentityProvider`, which
|
||||||
resolves nothing). alkhttp's server resolves tokens through exactly
|
resolves nothing). alkhttp's server resolves tokens through exactly
|
||||||
this one backend for every inbound surface (bearer + door-Basic,
|
this one backend for every inbound surface (bearer + door-Basic,
|
||||||
@@ -53,13 +53,13 @@ already-verified key.
|
|||||||
|
|
||||||
1. russh `auth_publickey(user, public_key)` fires.
|
1. russh `auth_publickey(user, public_key)` fires.
|
||||||
2. alkssh computes `public_key.fingerprint()` via the `ssh-key` re-
|
2. alkssh computes `public_key.fingerprint()` via the `ssh-key` re-
|
||||||
export (`HashAlg::Sha256`, `SHA256:base64` OpenSSH format) —
|
export (`HashAlg::Sha256`, `SHA256:base64` OpenSSH format):
|
||||||
**fingerprint-format parity with the assembly layer's registered
|
**fingerprint-format parity with the assembly layer's registered
|
||||||
identities is a contract invariant and a POC-1 checkpoint** (the
|
identities is a contract invariant and a POC-1 checkpoint** (the
|
||||||
format is stable and self-describing, but the registration path is
|
format is stable and self-describing, but the registration path is
|
||||||
assembly-owned, so the exact string compare must be verified, not
|
assembly-owned, so the exact string compare must be verified, not
|
||||||
assumed).
|
assumed).
|
||||||
3. `IdentityProvider::resolve_from_fingerprint(fp)` — `Some(identity)`
|
3. `IdentityProvider::resolve_from_fingerprint(fp): `Some(identity)`
|
||||||
→ `Result<Auth, _>::Authenticate { .. }`; `None` → decline
|
→ `Result<Auth, _>::Authenticate { .. }`; `None` → decline
|
||||||
(`Reject`). The user name string is **ignored for identity** (it is
|
(`Reject`). The user name string is **ignored for identity** (it is
|
||||||
not the family identity model); whether the username is checked at
|
not the family identity model); whether the username is checked at
|
||||||
@@ -69,7 +69,7 @@ already-verified key.
|
|||||||
4. On success, the resolved identity rides the session:
|
4. On success, the resolved identity rides the session:
|
||||||
`auth_succeeded(session)` attaches it to alkssh's per-session
|
`auth_succeeded(session)` attaches it to alkssh's per-session
|
||||||
state; when the session is bridged over alkcall, the identity is
|
state; when the session is bridged over alkcall, the identity is
|
||||||
pushed with the once-only `Connection::set_identity(identity)` —
|
pushed with the once-only `Connection::set_identity(identity)`,
|
||||||
after which `AccessControl::check(peer_identity)` gates every
|
after which `AccessControl::check(peer_identity)` gates every
|
||||||
mapped channel open exactly as in alkhttp (visible-surface =
|
mapped channel open exactly as in alkhttp (visible-surface =
|
||||||
authorized-surface). Unresolvable keys decline fail-closed; the
|
authorized-surface). Unresolvable keys decline fail-closed; the
|
||||||
@@ -81,20 +81,20 @@ ownership unverified there); the constant-rejection-time contract
|
|||||||
(`Config::auth_rejection_time`) plus `Config::max_auth_attempts`
|
(`Config::auth_rejection_time`) plus `Config::max_auth_attempts`
|
||||||
covers the timing/DoS posture. Note russh's own doc warning: seeing an
|
covers the timing/DoS posture. Note russh's own doc warning: seeing an
|
||||||
unknown key at the `offered` stage should not count toward the attempt
|
unknown key at the `offered` stage should not count toward the attempt
|
||||||
limit — the offered hook's disposition is "accept the offer, let
|
limit; the offered hook's disposition is "accept the offer, let
|
||||||
`auth_publickey` decide", which keeps counting honest.
|
`auth_publickey` decide", which keeps counting honest.
|
||||||
|
|
||||||
### Auth-method disposition (fail-closed matrix)
|
### Auth-method disposition (fail-closed matrix)
|
||||||
|
|
||||||
| russh hook | Disposition | Rationale |
|
| russh hook | Disposition | Rationale |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `auth_publickey` | Implement — the only method | The family identity model is keys; fingerprint → `Identity` |
|
| `auth_publickey` | Implement (the only method) | The family identity model is keys; fingerprint → `Identity` |
|
||||||
| `auth_publickey_offered` | Implement (accept-and-let-decide) | Keeps `max_auth_attempts` honest |
|
| `auth_publickey_offered` | Implement (accept-and-let-decide) | Keeps `max_auth_attempts` honest |
|
||||||
| `auth_none` | Decline (implement explicitly) | Fail-closed is ours to enforce |
|
| `auth_none` | Decline (implement explicitly) | Fail-closed is ours to enforce |
|
||||||
| `auth_password` | Decline (implement explicitly) | No password concept in family identity |
|
| `auth_password` | Decline (implement explicitly) | No password concept in family identity |
|
||||||
| `auth_keyboard_interactive` | Decline (implement explicitly) | Same |
|
| `auth_keyboard_interactive` | Decline (implement explicitly) | Same |
|
||||||
| `auth_openssh_certificate` | Decline initially (OQ candidate) | OpenSSH CA trust model ≠ family key registry; revisit on a use case |
|
| `auth_openssh_certificate` | Decline initially (OQ candidate) | OpenSSH CA trust model ≠ family key registry; revisit on a use case |
|
||||||
| `authentication_banner` | Assembly-provided (e.g. alkforge notice) or none | — |
|
| `authentication_banner` | Assembly-provided (e.g. alkforge notice) or none | (n/a) |
|
||||||
|
|
||||||
`Config::methods: MethodSet` must advertise exactly what is implemented
|
`Config::methods: MethodSet` must advertise exactly what is implemented
|
||||||
(honest advertisement = only `publickey` in every current configuration).
|
(honest advertisement = only `publickey` in every current configuration).
|
||||||
@@ -103,7 +103,7 @@ limit — the offered hook's disposition is "accept the offer, let
|
|||||||
corrects an earlier working assumption):** the default impls of
|
corrects an earlier working assumption):** the default impls of
|
||||||
`auth_none`, `auth_password`, `auth_keyboard_interactive`,
|
`auth_none`, `auth_password`, `auth_keyboard_interactive`,
|
||||||
`auth_publickey`, and `auth_openssh_certificate` are *reject*
|
`auth_publickey`, and `auth_openssh_certificate` are *reject*
|
||||||
(`Auth::reject()`) in 0.64.1 — only `auth_publickey_offered` defaults
|
(`Auth::reject()`) in 0.64.1; only `auth_publickey_offered` defaults
|
||||||
to accept. The fail-closed posture is therefore largely inherited at
|
to accept. The fail-closed posture is therefore largely inherited at
|
||||||
this pin, but every hook is still implemented explicitly as a family
|
this pin, but every hook is still implemented explicitly as a family
|
||||||
contract (a future pin bump could alter defaults; the pin is a one-way
|
contract (a future pin bump could alter defaults; the pin is a one-way
|
||||||
@@ -116,7 +116,7 @@ client *signs* with a private key; the server verifies. alkssh's client
|
|||||||
side therefore holds secret material, and the family rule applies: that
|
side therefore holds secret material, and the family rule applies: that
|
||||||
material comes from alkvault-backed sources, never env vars, never
|
material comes from alkvault-backed sources, never env vars, never
|
||||||
plaintext at rest (family ADR-002 decision 5). The guiding framing:
|
plaintext at rest (family ADR-002 decision 5). The guiding framing:
|
||||||
**a held SSH key is just another bearer-token-class secret** — opaque
|
**a held SSH key is just another bearer-token-class secret**: opaque
|
||||||
bytes the family holds, vault-encrypted at rest, decrypted at use.
|
bytes the family holds, vault-encrypted at rest, decrypted at use.
|
||||||
|
|
||||||
### The alkhttp pattern being mirrored (alkhttp ADR-014)
|
### The alkhttp pattern being mirrored (alkhttp ADR-014)
|
||||||
@@ -127,28 +127,28 @@ injects the secret as an alkcall `Capabilities` entry at registration,
|
|||||||
and the adapter reads it fail-closed at use time (`Capabilities::get`
|
and the adapter reads it fail-closed at use time (`Capabilities::get`
|
||||||
→ absent → loud internal error, never an unauthenticated send). The
|
→ absent → loud internal error, never an unauthenticated send). The
|
||||||
alkssh translation: the client adapter takes *key sources*, not static
|
alkssh translation: the client adapter takes *key sources*, not static
|
||||||
keys — the assembly layer supplies an alkvault-backed source (the
|
keys: the assembly layer supplies an alkvault-backed source (the
|
||||||
encrypted-at-rest blob); the adapter decodes it into a russh-usable
|
encrypted-at-rest blob); the adapter decodes it into a russh-usable
|
||||||
key at dial time; absent/locked vault → fail-closed decline, no
|
key at dial time; absent/locked vault → fail-closed decline, no
|
||||||
anonymous dial.
|
anonymous dial.
|
||||||
|
|
||||||
### The gap: alkvault-backed key storage
|
### The gap: alkvault-backed key storage
|
||||||
|
|
||||||
alkvault's role for SSH keys is **at-rest encryption, nothing more** —
|
alkvault's role for SSH keys is **at-rest encryption, nothing more**:
|
||||||
it is to an SSH client key what it is to every outgoing secret: a
|
it is to an SSH client key what it is to every outgoing secret: a
|
||||||
secret store. Held key material is treated exactly like a random
|
secret store. Held key material is treated exactly like a random
|
||||||
bearer token — opaque bytes the family holds, vault-encrypted at rest,
|
bearer token: opaque bytes the family holds, vault-encrypted at rest,
|
||||||
decrypted at use (the alkhttp ADR-014 model: vault is assembly-layer
|
decrypted at use (the alkhttp ADR-014 model: vault is assembly-layer
|
||||||
only, secrets are zeroized `Secret`s at the adapter, never env vars).
|
only, secrets are zeroized `Secret`s at the adapter, never env vars).
|
||||||
alkvault's *derivation* surface (`derive_ed25519`, SLIP-0010 paths) is
|
alkvault's *derivation* surface (`derive_ed25519`, SLIP-0010 paths) is
|
||||||
a different feature of that crate and is **not** the SSH-keys path —
|
a different feature of that crate and is **not** the SSH-keys path:
|
||||||
there is no derivation story here, and no vault-custody design where
|
there is no derivation story here, and no vault-custody design where
|
||||||
key bytes never leave the vault.
|
key bytes never leave the vault.
|
||||||
|
|
||||||
The bridge that does not exist yet, and is alkssh phase 0/1 design
|
The bridge that does not exist yet, and is alkssh phase 0/1 design
|
||||||
work:
|
work:
|
||||||
|
|
||||||
- **At-rest shape**: what byte format sits in the vault store —
|
- **At-rest shape**: what byte format sits in the vault store:
|
||||||
OpenSSH private-key encoding (the format `ssh-key` itself supports
|
OpenSSH private-key encoding (the format `ssh-key` itself supports
|
||||||
with a passphrase) is the natural candidate; alkvault `encrypt`/
|
with a passphrase) is the natural candidate; alkvault `encrypt`/
|
||||||
`decrypt` (`EncryptedData`) is the outer wrapper.
|
`decrypt` (`EncryptedData`) is the outer wrapper.
|
||||||
@@ -156,13 +156,13 @@ work:
|
|||||||
(`decode_secret_key` for OpenSSH encoding) at dial/construction
|
(`decode_secret_key` for OpenSSH encoding) at dial/construction
|
||||||
time, held as a zeroized `Secret`-like value for the dial's
|
time, held as a zeroized `Secret`-like value for the dial's
|
||||||
lifetime. russh's `Signer` trait was considered as an
|
lifetime. russh's `Signer` trait was considered as an
|
||||||
extraction-avoidance shape; withdrawn — the at-rest model implies
|
extraction-avoidance shape; withdrawn, since the at-rest model implies
|
||||||
the bytes are handled client-side at use time anyway (as every
|
the bytes are handled client-side at use time anyway (as every
|
||||||
bearer-token-like secret is), so a one-time decode at the adapter
|
bearer-token-like secret is), so a one-time decode at the adapter
|
||||||
boundary is the honest shape. Lifecycle questions (zeroization
|
boundary is the honest shape. Lifecycle questions (zeroization
|
||||||
bounds, locked-vault behavior → fail-closed no-dial) are POC-2's
|
bounds, locked-vault behavior → fail-closed no-dial) are POC-2's
|
||||||
checkpoints.
|
checkpoints.
|
||||||
- **Source shape**: the adapter takes *key sources*, not static keys —
|
- **Source shape**: the adapter takes *key sources*, not static keys:
|
||||||
an alknet-`main`-shaped `KeySource` seam
|
an alknet-`main`-shaped `KeySource` seam
|
||||||
(`{ File, Memory }`-equivalent) backed by vault-encrypted material
|
(`{ File, Memory }`-equivalent) backed by vault-encrypted material
|
||||||
at the assembly layer, mirroring alkhttp's capability injection.
|
at the assembly layer, mirroring alkhttp's capability injection.
|
||||||
@@ -171,14 +171,14 @@ work:
|
|||||||
comparison; note alkvault's `encrypt`/`decrypt` speak `String` only,
|
comparison; note alkvault's `encrypt`/`decrypt` speak `String` only,
|
||||||
so binary formats need a base64 layer); one-time
|
so binary formats need a base64 layer); one-time
|
||||||
`decode_secret_key` at dial confirmed; ssh-key zeroizes ed25519 key
|
`decode_secret_key` at dial confirmed; ssh-key zeroizes ed25519 key
|
||||||
bytes on drop but the vault's `decrypt` returns a plain `String` —
|
bytes on drop but the vault's `decrypt` returns a plain `String`,
|
||||||
the adapter owns plaintext zeroization at the seam (`Zeroizing`
|
so the adapter owns plaintext zeroization at the seam (`Zeroizing`
|
||||||
wrap) [OQ seed]; the `Signer` seam is dismissed with evidence (no
|
wrap) [OQ seed]; the `Signer` seam is dismissed with evidence (no
|
||||||
provided impl in 0.64.1; works when hand-rolled; in-process vault
|
provided impl in 0.64.1; works when hand-rolled; in-process vault
|
||||||
gains nothing). Locked/absent/tampered/rotation all fail closed
|
gains nothing). Locked/absent/tampered/rotation all fail closed
|
||||||
through distinct, mappable error shapes.
|
through distinct, mappable error shapes.
|
||||||
- **Scope note**: the vault is **client-side only**. The server side
|
- **Scope note**: the vault is **client-side only**. The server side
|
||||||
never touches the vault for *peer* identity — it receives clients'
|
never touches the vault for *peer* identity: it receives clients'
|
||||||
public keys and resolves fingerprints through the assembly layer's
|
public keys and resolves fingerprints through the assembly layer's
|
||||||
`IdentityProvider`. The server's own host key, though, is also a
|
`IdentityProvider`. The server's own host key, though, is also a
|
||||||
held secret and uses the same at-rest model. Vault material is for
|
held secret and uses the same at-rest model. Vault material is for
|
||||||
@@ -187,9 +187,9 @@ work:
|
|||||||
### Host keys and `check_server_key`
|
### Host keys and `check_server_key`
|
||||||
|
|
||||||
The server's host identity (`Config::keys: Vec<PrivateKey>`,
|
The server's host identity (`Config::keys: Vec<PrivateKey>`,
|
||||||
`certificates`) is assembly-configured — a held secret exactly like a
|
`certificates`) is assembly-configured, a held secret exactly like a
|
||||||
client key, same at-rest model as above. The client's
|
client key, same at-rest model as above. The client's
|
||||||
`Handler::check_server_key` defaults to *reject all* — russh forces a
|
`Handler::check_server_key` defaults to *reject all*: russh forces a
|
||||||
deliberate host-key policy. Family posture (no TOFU): pin expected
|
deliberate host-key policy. Family posture (no TOFU): pin expected
|
||||||
host fingerprints at the assembly layer; `check_known_hosts` exists in
|
host fingerprints at the assembly layer; `check_known_hosts` exists in
|
||||||
russh, but the family identity registry is likely the better pin store
|
russh, but the family identity registry is likely the better pin store
|
||||||
@@ -200,7 +200,7 @@ for the ADR.
|
|||||||
**Post-POC refinement (client-direction host-policy cases; see
|
**Post-POC refinement (client-direction host-policy cases; see
|
||||||
`reverse-runner.md` and OQ-05 in the architecture tracker):** the
|
`reverse-runner.md` and OQ-05 in the architecture tracker):** the
|
||||||
no-TOFU rule is a rule about trust *anchors*, not an absolute. Two
|
no-TOFU rule is a rule about trust *anchors*, not an absolute. Two
|
||||||
client cases: (1) hosts the assembly controls or can provision — the
|
client cases: (1) hosts the assembly controls or can provision; the
|
||||||
host key is knowable beforehand (family-built runners mint theirs at
|
host key is knowable beforehand (family-built runners mint theirs at
|
||||||
assembly time) → pinned, hard-decline on mismatch; (2) hosts that are
|
assembly time) → pinned, hard-decline on mismatch; (2) hosts that are
|
||||||
unknown until first contact (rental-fleet vanilla `sshd`) → the
|
unknown until first contact (rental-fleet vanilla `sshd`) → the
|
||||||
@@ -214,16 +214,16 @@ policy-blind.
|
|||||||
|
|
||||||
- alkcall: `src/core/auth.rs` (Identity/AuthToken/AuthContext/
|
- alkcall: `src/core/auth.rs` (Identity/AuthToken/AuthContext/
|
||||||
IdentityProvider verbatim above), `src/core/types.rs` (`set_identity`
|
IdentityProvider verbatim above), `src/core/types.rs` (`set_identity`
|
||||||
once-only), `src/protocol/dispatch.rs` (`resolve_identity` — token
|
once-only), `src/protocol/dispatch.rs` (`resolve_identity`: token
|
||||||
wins per-request, else connection identity), `src/registry/spec.rs`
|
wins per-request, else connection identity), `src/registry/spec.rs`
|
||||||
(`AccessControl::check`)
|
(`AccessControl::check`)
|
||||||
- alkcall ADRs: 002 (ProtocolHandler, hybrid auth completion —
|
- alkcall ADRs: 002 (ProtocolHandler, hybrid auth completion,
|
||||||
"handlers must not assume AuthContext is fully resolved"), 003
|
"handlers must not assume AuthContext is fully resolved"), 003
|
||||||
(auth-as-shared-core; the SshAdapter credential-table row), 006
|
(auth-as-shared-core; the SshAdapter credential-table row), 006
|
||||||
(AuthContext shape/set_identity), 010 (secret-material flow)
|
(AuthContext shape/set_identity), 010 (secret-material flow)
|
||||||
- alkhttp ADR-014 (secret material flow + capability injection — the
|
- alkhttp ADR-014 (secret material flow + capability injection, the
|
||||||
client-side pattern), ADR-073 (one backend behind every door surface)
|
client-side pattern), ADR-073 (one backend behind every door surface)
|
||||||
- alkvault 0.1.0: `src/service.rs` (`VaultServiceHandle` — for SSH
|
- alkvault 0.1.0: `src/service.rs` (`VaultServiceHandle`: for SSH
|
||||||
keys, the `encrypt`/`decrypt`/`EncryptedData` at-rest surface is the
|
keys, the `encrypt`/`decrypt`/`EncryptedData` at-rest surface is the
|
||||||
relevant one; the derivation surface is not on this path),
|
relevant one; the derivation surface is not on this path),
|
||||||
`src/protocol.rs` (`EncryptedData`)
|
`src/protocol.rs` (`EncryptedData`)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Mapping table — SSH channel types × family protocol seams
|
# Mapping table: SSH channel types × family protocol seams
|
||||||
|
|
||||||
Status: Draft (phase 0). The crate's spine. Every row maps a
|
Status: Draft (phase 0). The crate's spine. Every row maps a
|
||||||
russh server-side entry point onto exactly one family protocol seam
|
russh server-side entry point onto exactly one family protocol seam
|
||||||
plus exactly one cargo feature. Names below are *feature-name
|
plus exactly one cargo feature. Names below are *feature-name
|
||||||
candidates*, not decisions — the feature graph is phase 1 ADR
|
candidates*, not decisions; the feature graph is phase 1 ADR
|
||||||
territory. All downstream surfaces verified against published sibling
|
territory. All downstream surfaces verified against published sibling
|
||||||
source (alktty 0.8.1, alktunnels, alksocks, alkgit 0.1.0, alkcall 0.10).
|
source (alktty 0.8.1, alktunnels, alksocks, alkgit 0.1.0, alkcall 0.10).
|
||||||
|
|
||||||
@@ -16,19 +16,19 @@ run are marked **[POC-3 corrected]** below.
|
|||||||
## The shape that makes this work
|
## The shape that makes this work
|
||||||
|
|
||||||
Every family protocol crate already exposes a generic duplex-stream or
|
Every family protocol crate already exposes a generic duplex-stream or
|
||||||
`Connection`-serving entry — no crate needs to know SSH exists:
|
`Connection`-serving entry, so no crate needs to know SSH exists:
|
||||||
|
|
||||||
| Crate | Serve-over-a-stream entry | Signature shape |
|
| Crate | Serve-over-a-stream entry | Signature shape |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| alktty | `drive_session[_pre_negotiated]` | `(impl AsyncWrite, impl AsyncRead, backends, identity)` |
|
| alktty | `drive_session[_pre_negotiated]` | `(impl AsyncWrite, impl AsyncRead, backends, identity)` |
|
||||||
| alkgit | `serve_duplex_resolved` | `(repo_id, identity, service, T: AsyncRead+AsyncWrite, limits, backend traits)` |
|
| alkgit | `serve_duplex_resolved` | `(repo_id, identity, service, T: AsyncRead+AsyncWrite, limits, backend traits)` |
|
||||||
| alksocks | `serve_socks5_over` | `(T: AsyncRead+AsyncWrite+Unpin, dial, egress, accept, can_bind)` |
|
| alksocks | `serve_socks5_over` | `(T: AsyncRead+AsyncWrite+Unpin, dial, egress, accept, can_bind)` |
|
||||||
| alktunnels | — **no serve-over-stream entry; see absence note** | consumer wrappers / open-op registrations instead |
|
| alktunnels | **no serve-over-stream entry; see absence note** | consumer wrappers / open-op registrations instead |
|
||||||
|
|
||||||
russh channels convert to duplex via `Channel::{into_stream, split,
|
russh channels convert to duplex via `Channel::{into_stream, split,
|
||||||
make_reader/make_writer}` (`ChannelStream`/com halves). So each
|
make_reader/make_writer}` (`ChannelStream`/com halves). So each
|
||||||
mapping is: accept-or-decline the channel open
|
mapping is: accept-or-decline the channel open
|
||||||
(`ChannelOpenHandle`; dropping it auto-rejects — russh's default is
|
(`ChannelOpenHandle`; dropping it auto-rejects, russh's default is
|
||||||
already fail-closed), pump/serve the channel through the seam, map the
|
already fail-closed), pump/serve the channel through the seam, map the
|
||||||
request-level extras.
|
request-level extras.
|
||||||
|
|
||||||
@@ -36,16 +36,16 @@ request-level extras.
|
|||||||
|
|
||||||
| SSH channel type | Vanilla use | Feature (candidate) | Family seam | Notes |
|
| SSH channel type | Vanilla use | Feature (candidate) | Family seam | Notes |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `session` → `pty_request`+`shell_request` | `ssh host` interactive shell | `session` | alktty producer: `NegotiateRequest` constructed locally → `drive_session` (or pre-allocated variant) | `pty_request` args → `TerminalParamsWire` (term/cols/rows/pixels; SSH modes → `modes` JSON reserved); shell case needs a cmd convention (OQ); PTY mode merges stderr into stdout (kernel PTY). **[POC-3 corrected]** the data-plane mapping is a *translation bridge*, not a byte pump: alktty's plane is chunk-framed both directions (stdin/stdout/stderr + CTRL_IN/CTRL_OUT chunks), so alkssh encodes SSH data→stdin chunks and decodes stdout→data, stderr→extended_data, exit chunk→exit-status/exit-signal. The exit chunk encodes pipe-path signal termination as the negative code `−N` (alktty ADR-004's convention — `pipe.rs::exit_code_from`); the PTY path reduces signal termination to a plain exit code (portable-pty's exit surface), so PTY-mode `exit-signal` fidelity is an alktty-backend question |
|
| `session` → `pty_request`+`shell_request` | `ssh host` interactive shell | `session` | alktty producer: `NegotiateRequest` constructed locally → `drive_session` (or pre-allocated variant) | `pty_request` args → `TerminalParamsWire` (term/cols/rows/pixels; SSH modes → `modes` JSON reserved); shell case needs a cmd convention (OQ); PTY mode merges stderr into stdout (kernel PTY). **[POC-3 corrected]** the data-plane mapping is a *translation bridge*, not a byte pump: alktty's plane is chunk-framed both directions (stdin/stdout/stderr + CTRL_IN/CTRL_OUT chunks), so alkssh encodes SSH data→stdin chunks and decodes stdout→data, stderr→extended_data, exit chunk→exit-status/exit-signal. The exit chunk encodes pipe-path signal termination as the negative code `−N` (alktty ADR-004's convention, `pipe.rs::exit_code_from`); the PTY path reduces signal termination to a plain exit code (portable-pty's exit surface), so PTY-mode `exit-signal` fidelity is an alktty-backend question |
|
||||||
| `session` → `exec_request` (non-git command) | `ssh host "cmd"` | `session` | alktty pipe mode: `terminal: None`, separate stderr | stderr flows back as SSH `extended_data` code 1 — only valid without PTY |
|
| `session` → `exec_request` (non-git command) | `ssh host "cmd"` | `session` | alktty pipe mode: `terminal: None`, separate stderr | stderr flows back as SSH `extended_data` code 1; only valid without PTY |
|
||||||
| `session` → `exec_request`/`subsystem_request` git | `git push/clone ssh://` | `git` | alkgit: parse command → `Service` + repo (same grammar as `preamble.rs`, without `\0host=`) → `serve_duplex_resolved(repo_id, identity, service, duplex, ...)` | Process-mode dialect is correct (SSH supplies service+repo out of band; server speaks first = advertisement). **[POC-3 corrected]** git over SSH is exec-only: a `subsystem` request carries the service *name* but no repo id, so even a git-service subsystem must decline — vanilla git always executes `git-upload-pack '/repo'`. POC-3: vanilla clone/push/second-push through the published seam + substrate composition, exit-status in both directions |
|
| `session` → `exec_request`/`subsystem_request` git | `git push/clone ssh://` | `git` | alkgit: parse command → `Service` + repo (same grammar as `preamble.rs`, without `\0host=`) → `serve_duplex_resolved(repo_id, identity, service, duplex, ...)` | Process-mode dialect is correct (SSH supplies service+repo out of band; server speaks first = advertisement). **[POC-3 corrected]** git over SSH is exec-only: a `subsystem` request carries the service *name* but no repo id, so even a git-service subsystem must decline, as vanilla git always executes `git-upload-pack '/repo'`. POC-3: vanilla clone/push/second-push through the published seam + substrate composition, exit-status in both directions |
|
||||||
| `session` → SOCKS5-in-channel | (n/a — retired) | ~~`dynamic-forward`~~ **retired (ADR-003 §1a)** | was alksocks: `serve_socks5_over` | **Post-architecture retirement:** no consumer is ever the family-native client shape this row served. Vanilla `ssh -D` does SOCKS client-side and opens plain `direct-tcpip` (POC-3 correction 2 → the `tunnel` row); the family `-D` behavior is a *client-local* alksocks door over the alkssh client's `direct-tcpip` opens (assembly composition). POC-3 stage B remains the evidence that SOCKS-in-channel is feasible if a family client ever wants a remote SOCKS door (back via explicit ADR). **[ADR-003 amended]** |
|
| `session` → SOCKS5-in-channel | (n/a, retired) | ~~`dynamic-forward`~~ **retired (ADR-003 §1a)** | was alksocks: `serve_socks5_over` | **Post-architecture retirement:** no consumer is ever the family-native client shape this row served. Vanilla `ssh -D` does SOCKS client-side and opens plain `direct-tcpip` (POC-3 correction 2 → the `tunnel` row); the family `-D` behavior is a *client-local* alksocks door over the alkssh client's `direct-tcpip` opens (assembly composition). POC-3 stage B remains the evidence that SOCKS-in-channel is feasible if a family client ever wants a remote SOCKS door (back via explicit ADR). **[ADR-003 amended]** |
|
||||||
| `direct-tcpip` | `ssh -L` | `tunnel` | alktunnels (two routes, below) | Target = `SubstrateAddr::HostPort`/`Path`; `Substrate` = Tcp/Unix |
|
| `direct-tcpip` | `ssh -L` | `tunnel` | alktunnels (two routes, below) | Target = `SubstrateAddr::HostPort`/`Path`; `Substrate` = Tcp/Unix |
|
||||||
| `forwarded-tcpip` | `ssh -R` (data channel) | `tunnel` | alktunnels: `register_tunnel_forwarded_openable`'s `ForwardedHandler(params, BiStream)` shape — in the adapter, hand the russh channel duplex to the handler | listen flow via global-request rows below |
|
| `forwarded-tcpip` | `ssh -R` (data channel) | `tunnel` | alktunnels: `register_tunnel_forwarded_openable`'s `ForwardedHandler(params, BiStream)` shape; in the adapter, hand the russh channel duplex to the handler | listen flow via global-request rows below |
|
||||||
| `direct-streamlocal` | unix-socket forward | `tunnel` | same `SubstrateAddr::Path` treatment | unix-only; decline on other platforms (compile-time feature + runtime decline) |
|
| `direct-streamlocal` | unix-socket forward | `tunnel` | same `SubstrateAddr::Path` treatment | unix-only; decline on other platforms (compile-time feature + runtime decline) |
|
||||||
| `x11` | X forwarding | **never** (no feature) | — | decline, always; not even feature-gated initially (OQ: revisit years later if a use case arrives) |
|
| `x11` | X forwarding | **never** (no feature) | none | decline, always; not even feature-gated initially (OQ: revisit years later if a use case arrives) |
|
||||||
| `sftp` (`subsystem_request("sftp")`) | sftp clients | `sftp` (future) | alkfs (deferred, family ADR-002) | The declinable placeholder — the feature name exists in the feature graph, decline until alkfs lands |
|
| `sftp` (`subsystem_request("sftp")`) | sftp clients | `sftp` (future) | alkfs (deferred, family ADR-002) | The declinable placeholder: the feature name exists in the feature graph, decline until alkfs lands |
|
||||||
| `auth-agent` (`agent_request`) | ssh-agent forwarding | **never** | — | decline; family holds keys in alkvault — forwarding agents to remote sessions is the anti-pattern this posture rejects |
|
| `auth-agent` (`agent_request`) | ssh-agent forwarding | **never** | none | decline; family holds keys in alkvault, since forwarding agents to remote sessions is the anti-pattern this posture rejects |
|
||||||
|
|
||||||
Notes: the rows deliberately split one channel type (`session`) across
|
Notes: the rows deliberately split one channel type (`session`) across
|
||||||
three features, because vanilla SSH multiplexes everything through a
|
three features, because vanilla SSH multiplexes everything through a
|
||||||
@@ -62,20 +62,20 @@ a shell, a pipe-mode command. The failure matrix this produces:
|
|||||||
Dispatch is parse-then-route by command shape: parsing to a git
|
Dispatch is parse-then-route by command shape: parsing to a git
|
||||||
service and repo requires `git`; otherwise the alktty pipe/pty path
|
service and repo requires `git`; otherwise the alktty pipe/pty path
|
||||||
requires `session`. Parsing happens before dispatch and before any
|
requires `session`. Parsing happens before dispatch and before any
|
||||||
resource allocation (a leaked channel alloc is a capability leak —
|
resource allocation (a leaked channel alloc is a capability leak,
|
||||||
decline first). (Post-architecture: the former SOCKS-detect dispatch
|
so decline first).. (Post-architecture: the former SOCKS-detect dispatch
|
||||||
arm is retired with the `dynamic-forward` feature — ADR-003 §1a /
|
arm is retired with the `dynamic-forward` feature, per ADR-003 §1a /
|
||||||
ADR-006 amendment; request-less session channels decline.) Grammar/
|
ADR-006 amendment; request-less session channels decline.) Grammar/
|
||||||
precedence per ADR-006.
|
precedence per ADR-006.
|
||||||
|
|
||||||
## Global-request rows (`tcpip_forward` / `streamlocal_forward`) — the `-R` listen flow
|
## Global-request rows (`tcpip_forward` / `streamlocal_forward`): the `-R` listen flow
|
||||||
|
|
||||||
`Handler::tcpip_forward(address, &mut port) -> bool` + per-open
|
`Handler::tcpip_forward(address, &mut port) -> bool` + per-open
|
||||||
`channel_open_forwarded_tcpip`. Consumer-side alktunnels:
|
`channel_open_forwarded_tcpip`. Consumer-side alktunnels:
|
||||||
`TunnelListener`, `open_reverse_channel`; the forwarded data channel
|
`TunnelListener`, `open_reverse_channel`; the forwarded data channel
|
||||||
maps to the `ForwardedHandler` route above. Dispositions follow the
|
maps to the `ForwardedHandler` route above. Dispositions follow the
|
||||||
`tunnel` feature (decline both listen and data when `tunnel` absent).
|
`tunnel` feature (decline both listen and data when `tunnel` absent).
|
||||||
`port = 0` (server picks) interacts with the `&mut u32` out-param —
|
`port = 0` (server picks) interacts with the `&mut u32` out-param;
|
||||||
spec detail deferred to phase 1.
|
spec detail deferred to phase 1.
|
||||||
|
|
||||||
## Request-level mapping on a `session` channel
|
## Request-level mapping on a `session` channel
|
||||||
@@ -83,33 +83,33 @@ spec detail deferred to phase 1.
|
|||||||
| SSH request | Family mapping |
|
| SSH request | Family mapping |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `pty_request` | `NegotiateRequest.tty = TerminalParamsWire{term, cols, rows, pixel_w, pixel_h, modes}` |
|
| `pty_request` | `NegotiateRequest.tty = TerminalParamsWire{term, cols, rows, pixel_w, pixel_h, modes}` |
|
||||||
| `shell_request` | `NegotiateRequest.cmd` per the backend's shell convention (OQ — probably `cmd` + `cwd`/`env` from assembly policy, not from the wire) |
|
| `shell_request` | `NegotiateRequest.cmd` per the backend's shell convention (OQ: probably `cmd` + `cwd`/`env` from assembly policy, not from the wire) |
|
||||||
| `exec_request` | Parse command → git/service dispatch or alktty pipe `cmd`; unknown commands fail with `session.channel_failure` (parse-then-decline, no resource alloc) |
|
| `exec_request` | Parse command → git/service dispatch or alktty pipe `cmd`; unknown commands fail with `session.channel_failure` (parse-then-decline, no resource alloc) |
|
||||||
| `subsystem_request` | `sftp` → decline until `sftp` feature serves alkfs; git subsystem names → same parse as exec; anything else → decline |
|
| `subsystem_request` | `sftp` → decline until `sftp` feature serves alkfs; git subsystem names → same parse as exec; anything else → decline |
|
||||||
| `env_request` | **Decline by default.** Wire-supplied env is not family identity config; `TtyParams.env` comes from assembly. (OQ: allowlist shape, e.g. `LANG`, later.) |
|
| `env_request` | **Decline by default.** Wire-supplied env is not family identity config; `TtyParams.env` comes from assembly. (OQ: allowlist shape, e.g. `LANG`, later.) |
|
||||||
| `window_change_request` | `TtyControlHandle::resize(cols, rows, pixel_w, pixel_h)` |
|
| `window_change_request` | `TtyControlHandle::resize(cols, rows, pixel_w, pixel_h)` |
|
||||||
| `signal` | `TtyControlHandle::signal(name)` (alktty's `signal_from_name` accepts the HUP/INT/TERM/... names verbatim) |
|
| `signal` | `TtyControlHandle::signal(name)` (alktty's `signal_from_name` accepts the HUP/INT/TERM/... names verbatim) |
|
||||||
| data / `extended_data(1)` | channel duplex in/out; stderr only meaningful in pipe mode (PTY merges) |
|
| data / `extended_data(1)` | channel duplex in/out; stderr only meaningful in pipe mode (PTY merges) |
|
||||||
| `exit_status` | from `TtyHandle.exit_code` (`TtySession.wait()` semantics: i32; negative = signal-terminated on the pipe path — `pipe.rs::exit_code_from` maps Unix `ExitStatus::signal()` to `−sig`; the PTY path reduces signal termination to a plain exit code, see the exit_signal row) — alkssh sends the SSH `exit-status` request before channel close (POC-3: proven both directions via the cloned `Session::handle()` from the spawned serving task); `−1` (alktty's could-not-determine sentinel, alktty ADR-004 §4) → `exit-status(255)` |
|
| `exit_status` | from `TtyHandle.exit_code` (`TtySession.wait()` semantics: i32; negative = signal-terminated on the pipe path; `pipe.rs::exit_code_from` maps Unix `ExitStatus::signal()` to `−sig`; the PTY path reduces signal termination to a plain exit code, see the exit_signal row), so alkssh sends the SSH `exit-status` request before channel close (POC-3: proven both directions via the cloned `Session::handle()` from the spawned serving task); `−1` (alktty's could-not-determine sentinel, alktty ADR-004 §4) → `exit-status(255)` |
|
||||||
| `exit_signal` | derived server-side from the exit chunk's negative code — `−N` → name via a Unix signal-number→name table; `core_dumped` never asserted (alktty discards `ExitStatus::core_dumped()`); no chunk change is needed or proposed (the encoding is alktty's documented contract, alknet ADR-055 lineage). POC-3 observed `−15` → `exit-signal("TERM")` live on the pipe path. PTY path: portable-pty 0.9's `ExitStatus` conversion yields `code = status.code().unwrap_or(1)` (**1**) for signal-terminated children, keeping the signal only in a separate humanized (`strsignal`) string field alktty's waiter discards — faithful `exit-signal` on the PTY row needs the alktty backend to conform to its own ADR convention (upstream ask: alktty/portable-pty trackers; the alkssh contract adapts automatically) |
|
| `exit_signal` | derived server-side from the exit chunk's negative code, `−N` → name via a Unix signal-number→name table; `core_dumped` never asserted (alktty discards `ExitStatus::core_dumped()`); no chunk change is needed or proposed (the encoding is alktty's documented contract, alknet ADR-055 lineage). POC-3 observed `−15` → `exit-signal("TERM")` live on the pipe path. PTY path: portable-pty 0.9's `ExitStatus` conversion yields `code = status.code().unwrap_or(1)` (**1**) for signal-terminated children, keeping the signal only in a separate humanized (`strsignal`) string field alktty's waiter discards, so faithful `exit-signal` on the PTY row needs the alktty backend to conform to its own ADR convention (upstream ask: alktty/portable-pty trackers; the alkssh contract adapts automatically) |
|
||||||
|
|
||||||
## Data-plane / limits contract
|
## Data-plane / limits contract
|
||||||
|
|
||||||
- russh windows: `Config::window_size`, `maximum_packet_size`,
|
- russh windows: `Config::window_size`, `maximum_packet_size`,
|
||||||
`channel_buffer_size` cap per-channel memory; `Limits` caps
|
`channel_buffer_size` cap per-channel memory; `Limits` caps
|
||||||
rekey volume; `inactivity_timeout` the session wall clock — the
|
rekey volume; `inactivity_timeout` the session wall clock, and the
|
||||||
family bounded-resources convention maps onto these knobs plus the
|
family bounded-resources convention maps onto these knobs plus the
|
||||||
crate's own max-concurrent-channels (per session) and the per-length
|
crate's own max-concurrent-channels (per session) and the per-length
|
||||||
parse limits on exec commands.
|
parse limits on exec commands.
|
||||||
- alkcall bridge leg (when the family half is remote): channels-layer
|
- alkcall bridge leg (when the family half is remote): channels-layer
|
||||||
reassembly cap 1 MiB/channel, chunk max 16 MiB, per-identity
|
reassembly cap 1 MiB/channel, chunk max 16 MiB, per-identity
|
||||||
channel-count policy — an SSH channel bridged over `alk/channels`
|
channel-count policy, so an SSH channel bridged over `alk/channels`
|
||||||
rides one reassembled byte stream per SSH channel; SSH's own
|
rides one reassembled byte stream per SSH channel; SSH's own
|
||||||
multiplexing is why ADR-034 grants `alk/ssh` only stream_types
|
multiplexing is why ADR-034 grants `alk/ssh` only stream_types
|
||||||
`[0, 1]` ("SSH multiplexes internally, including its own control").
|
`[0, 1]` ("SSH multiplexes internally, including its own control").
|
||||||
- Honest advertisement: `Config::methods` mirrors implemented auth; an
|
- Honest advertisement: `Config::methods` mirrors implemented auth; an
|
||||||
unfeatured channel type is *declined at open*, never half-opened.
|
unfeatured channel type is *declined at open*, never half-opened.
|
||||||
Vanilla clients see a clean failure — which is the contract.
|
Vanilla clients see a clean failure, which is the contract.
|
||||||
|
|
||||||
## alktunnels absence note
|
## alktunnels absence note
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ decision:
|
|||||||
`composed::direct_egress` is the family precedent for exactly this
|
`composed::direct_egress` is the family precedent for exactly this
|
||||||
bridging.)
|
bridging.)
|
||||||
2. **Registry-side bridge**: within an alkcall serving endpoint,
|
2. **Registry-side bridge**: within an alkcall serving endpoint,
|
||||||
`register_tunnel_direct_openable` with a `DialFn` — but that puts
|
`register_tunnel_direct_openable` with a `DialFn`, but that puts
|
||||||
alkssh-shaped work in the serving endpoint's registry, which
|
alkssh-shaped work in the serving endpoint's registry, which
|
||||||
inverts the door posture. Likely rejected on composability grounds;
|
inverts the door posture. Likely rejected on composability grounds;
|
||||||
recorded here for the ADR to decide explicitly.
|
recorded here for the ADR to decide explicitly.
|
||||||
@@ -136,17 +136,17 @@ decision:
|
|||||||
Three alkcall ADR constraints collide productively:
|
Three alkcall ADR constraints collide productively:
|
||||||
|
|
||||||
- ADR-004 reserves `alk/ssh` → `SshAdapter` (custom protocol).
|
- ADR-004 reserves `alk/ssh` → `SshAdapter` (custom protocol).
|
||||||
- ADR-034's per-ALPN table: `alk/ssh` gets stream_types `[0, 1]` —
|
- ADR-034's per-ALPN table: `alk/ssh` gets stream_types `[0, 1]`;
|
||||||
SSH multiplexes internally, the channels layer is byte-blind to it.
|
SSH multiplexes internally, the channels layer is byte-blind to it.
|
||||||
- ADR-042/051: "The hub never runs a handler for `alk/tty`, `alk/ssh`,
|
- ADR-042/051: "The hub never runs a handler for `alk/tty`, `alk/ssh`,
|
||||||
or `alk/tunnel`" — `alk/ssh` channels *relay payload-blind* between
|
or `alk/tunnel`": `alk/ssh` channels *relay payload-blind* between
|
||||||
spokes; ACL fires at `channel/open`.
|
spokes; ACL fires at `channel/open`.
|
||||||
|
|
||||||
ADR-009 additionally sketches a browser route: a WebTransport session
|
ADR-009 additionally sketches a browser route: a WebTransport session
|
||||||
to `/alk/ssh` where each bidi stream is handed to `SshAdapter::handle`
|
to `/alk/ssh` where each bidi stream is handed to `SshAdapter::handle`
|
||||||
as a `Connection`, with a WASM SSH parser speaking SSH over it.
|
as a `Connection`, with a WASM SSH parser speaking SSH over it.
|
||||||
|
|
||||||
Reading: `alk/ssh` is plausibly *SSH-over-channels* — the raw vanilla-
|
Reading: `alk/ssh` is plausibly *SSH-over-channels*: the raw vanilla-
|
||||||
SSH byte stream carried between alkssh instances (edge termination at
|
SSH byte stream carried between alkssh instances (edge termination at
|
||||||
one end, mapped serving at the other), reused by the browser route.
|
one end, mapped serving at the other), reused by the browser route.
|
||||||
Whether the edge-terminated topology (russh at the edge, family seams
|
Whether the edge-terminated topology (russh at the edge, family seams
|
||||||
@@ -160,11 +160,11 @@ phase 1 ADR-001 (ALPN pin, ledger rule 1).
|
|||||||
## Open questions seeded for phase 1
|
## Open questions seeded for phase 1
|
||||||
|
|
||||||
- Session-channel dispatch precedence + the exec-command grammar
|
- Session-channel dispatch precedence + the exec-command grammar
|
||||||
(git parse vs pipe cmd) — parse-then-fail-closed contract.
|
(git parse vs pipe cmd), a parse-then-fail-closed contract.
|
||||||
- Shell command convention for `shell_request` (assembly policy vs
|
- Shell command convention for `shell_request` (assembly policy vs
|
||||||
wire-supplied; `wd`/`env` provenance).
|
wire-supplied; `wd`/`env` provenance).
|
||||||
- `env_request` policy (decline-only vs allowlist).
|
- `env_request` policy (decline-only vs allowlist).
|
||||||
- OQ: OpenSSH certificate auth (`auth_openssh_certificate`) — decline
|
- OQ: OpenSSH certificate auth (`auth_openssh_certificate`): decline
|
||||||
until a use case.
|
until a use case.
|
||||||
- OQ: username disposition (ignored entirely vs a routing hint).
|
- OQ: username disposition (ignored entirely vs a routing hint).
|
||||||
- Feature-graph names + the `session`-vs-`git`-vs-`dynamic-forward`
|
- Feature-graph names + the `session`-vs-`git`-vs-`dynamic-forward`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# POC-1 findings — auth bridge + fail-closed declines end to end
|
# POC-1 findings: auth bridge + fail-closed declines end to end
|
||||||
|
|
||||||
Status: Complete (phase 0). POC source: `/workspace/alkssh-poc1`
|
Status: Complete (phase 0). POC source: `/workspace/alkssh-poc1`
|
||||||
(standalone crate, disposable — never merges). All hypotheses in
|
(standalone crate, disposable, never merges). All hypotheses in
|
||||||
`pocs.md` §POC-1 were confirmed; the doc below records the evidence,
|
`pocs.md` §POC-1 were confirmed; the doc below records the evidence,
|
||||||
one correction to the phase 0 research docs, two API-shape facts, and
|
one correction to the phase 0 research docs, two API-shape facts, and
|
||||||
the OQ seeds this POC produced.
|
the OQ seeds this POC produced.
|
||||||
@@ -11,8 +11,8 @@ the OQ seeds this POC produced.
|
|||||||
- russh **0.64.1** from crates.io (the current published release;
|
- russh **0.64.1** from crates.io (the current published release;
|
||||||
crates.io `newest_version` re-verified during the POC), default
|
crates.io `newest_version` re-verified during the POC), default
|
||||||
features off, crypto backend `ring`. Clean build under edition 2024,
|
features off, crypto backend `ring`. Clean build under edition 2024,
|
||||||
rustc 1.99.0. The `ring` backend built with no external provisioning
|
rustc 1.99.0. The `ring` backend built with no external provisioning,
|
||||||
— relevant data point for the pin ADR's crypto-backend question.
|
a relevant data point for the pin ADR's crypto-backend question.
|
||||||
- Dependency tree confirms the pin considerations in `russh.md`:
|
- Dependency tree confirms the pin considerations in `russh.md`:
|
||||||
`ssh-key 0.7.0-rc.11` exact-pinned and re-exported via
|
`ssh-key 0.7.0-rc.11` exact-pinned and re-exported via
|
||||||
`russh::keys::ssh_key`; `rand_core 0.10` in-tree. Key generation in
|
`russh::keys::ssh_key`; `rand_core 0.10` in-tree. Key generation in
|
||||||
@@ -22,20 +22,20 @@ the OQ seeds this POC produced.
|
|||||||
- Client side: russh client (`client::Handler`, `client::connect`) as
|
- Client side: russh client (`client::Handler`, `client::connect`) as
|
||||||
the primary programmatic client, plus a stub `IdentityProvider`
|
the primary programmatic client, plus a stub `IdentityProvider`
|
||||||
registered via fingerprints taken **from `ssh-keygen -lf` output**
|
registered via fingerprints taken **from `ssh-keygen -lf` output**
|
||||||
(independent OpenSSH implementation) — making the parity check a
|
(independent OpenSSH implementation), making the parity check a
|
||||||
real cross-implementation check, not a same-code self-check.
|
real cross-implementation check, not a same-code self-check.
|
||||||
- Vanilla client: OpenSSH 9.6p1 (`ssh`, `ssh -L`), isolated
|
- Vanilla client: OpenSSH 9.6p1 (`ssh`, `ssh -L`), isolated
|
||||||
`UserKnownHostsFile`, `BatchMode`, `IdentitiesOnly`.
|
`UserKnownHostsFile`, `BatchMode`, `IdentitiesOnly`.
|
||||||
|
|
||||||
## Validated: all checks pass (31/31)
|
## Validated: all checks pass (31/31)
|
||||||
|
|
||||||
### 1. Fingerprint-format parity — confirmed, format is exactly the registered string
|
### 1. Fingerprint-format parity confirmed: format is exactly the registered string
|
||||||
|
|
||||||
`public_key.fingerprint(HashAlg::Sha256).to_string()` (via the
|
`public_key.fingerprint(HashAlg::Sha256).to_string()` (via the
|
||||||
`ssh-key` re-export) yields `SHA256:<unpadded base64, standard
|
`ssh-key` re-export) yields `SHA256:<unpadded base64, standard
|
||||||
alphabet>` — byte-identical to `ssh-keygen -lf` on the same public key. (An
|
alphabet>`, byte-identical to `ssh-keygen -lf` on the same public key. (An
|
||||||
earlier phrasing of this section called the encoding "url-safe"; the
|
earlier phrasing of this section called the encoding "url-safe"; the
|
||||||
observed strings and OpenSSH's own format use the standard alphabet —
|
observed strings and OpenSSH's own format use the standard alphabet,
|
||||||
unpadded standard base64 is the invariant.) The
|
unpadded standard base64 is the invariant.) The
|
||||||
`resolve_from_fingerprint` string compare works with values that were
|
`resolve_from_fingerprint` string compare works with values that were
|
||||||
registered from OpenSSH tooling output. Parity was additionally
|
registered from OpenSSH tooling output. Parity was additionally
|
||||||
@@ -47,7 +47,7 @@ self-describing; the assembly layer can register `SHA256:` strings
|
|||||||
taken from any standard issuer (ssh-keygen, alkcall-side computation
|
taken from any standard issuer (ssh-keygen, alkcall-side computation
|
||||||
via the same re-export) and the server-side compare holds.
|
via the same re-export) and the server-side compare holds.
|
||||||
|
|
||||||
### 2. Auth bridge end to end — confirmed as designed
|
### 2. Auth bridge end to end (confirmed as designed)
|
||||||
|
|
||||||
- `auth_publickey` fires **after** russh's own signature verification
|
- `auth_publickey` fires **after** russh's own signature verification
|
||||||
(documented behavior observed: the vanilla key was accepted without
|
(documented behavior observed: the vanilla key was accepted without
|
||||||
@@ -59,14 +59,14 @@ via the same re-export) and the server-side compare holds.
|
|||||||
- `auth_publickey_offered` accept-and-let-decide works with OpenSSH's
|
- `auth_publickey_offered` accept-and-let-decide works with OpenSSH's
|
||||||
probe-then-sign flow: the hook fires (key seen, ownership unverified)
|
probe-then-sign flow: the hook fires (key seen, ownership unverified)
|
||||||
before the signed attempt, and **does not count toward
|
before the signed attempt, and **does not count toward
|
||||||
`max_auth_attempts`** — consistent with russh's own doc warning
|
`max_auth_attempts`**, consistent with russh's own doc warning
|
||||||
about keeping the count honest.
|
about keeping the count honest.
|
||||||
- Username: ignored entirely for identity (keys resolve independently
|
- Username: ignored entirely for identity (keys resolve independently
|
||||||
of the username string sent by the client) — supports the
|
of the username string sent by the client), which supports the
|
||||||
"ignore username entirely" OQ direction; `ssh anything@host`
|
"ignore username entirely" OQ direction; `ssh anything@host`
|
||||||
compatibility is unaffected.
|
compatibility is unaffected.
|
||||||
|
|
||||||
### 3. Decline matrix — every method declined and proven
|
### 3. Decline matrix: every method declined and proven
|
||||||
|
|
||||||
- `auth_none` / `auth_password` / `auth_keyboard_interactive` all
|
- `auth_none` / `auth_password` / `auth_keyboard_interactive` all
|
||||||
observed declining (russh client forced each; vanilla client with
|
observed declining (russh client forced each; vanilla client with
|
||||||
@@ -84,10 +84,10 @@ Only `auth_publickey_offered` defaults to accept. The research assumed
|
|||||||
older-russh accept-defaults. The fail-closed discipline at 0.64.1 is
|
older-russh accept-defaults. The fail-closed discipline at 0.64.1 is
|
||||||
therefore largely *inherited*; the family contract still requires
|
therefore largely *inherited*; the family contract still requires
|
||||||
implementing every hook explicitly (a future russh bump could change
|
implementing every hook explicitly (a future russh bump could change
|
||||||
defaults — the pin is a one-way door) and POC-1 proves the explicit
|
defaults; the pin is a one-way door) and POC-1 proves the explicit
|
||||||
implementations end to end.
|
implementations end to end.
|
||||||
|
|
||||||
### 4. Bounded-resource auth behavior — `max_auth_attempts` semantics
|
### 4. Bounded-resource auth behavior: `max_auth_attempts` semantics
|
||||||
|
|
||||||
The **initial `none` probe counts toward `max_auth_attempts`**: with a
|
The **initial `none` probe counts toward `max_auth_attempts`**: with a
|
||||||
limit of 3, a BatchMode vanilla client got the `none` probe plus two
|
limit of 3, a BatchMode vanilla client got the `none` probe plus two
|
||||||
@@ -101,7 +101,7 @@ signed declined attempts and was disconnected with
|
|||||||
mind (e.g. limit 10 default is comfortable; a small limit eats one
|
mind (e.g. limit 10 default is comfortable; a small limit eats one
|
||||||
attempt on every connection).
|
attempt on every connection).
|
||||||
|
|
||||||
### 5. Fail-closed channel declines observable by vanilla ssh — confirmed
|
### 5. Fail-closed channel declines observable by vanilla ssh (confirmed)
|
||||||
|
|
||||||
- `ChannelOpenHandle` **drop = auto-reject** sends
|
- `ChannelOpenHandle` **drop = auto-reject** sends
|
||||||
`AdministrativelyProhibited` (verified in russh source
|
`AdministrativelyProhibited` (verified in russh source
|
||||||
@@ -109,16 +109,16 @@ signed declined attempts and was disconnected with
|
|||||||
explicit reject call needed anywhere: unfeatured channel types are
|
explicit reject call needed anywhere: unfeatured channel types are
|
||||||
handled by simply not accepting the handle.
|
handled by simply not accepting the handle.
|
||||||
- Vanilla `ssh host` after successful auth: `channel 0: open failed:
|
- Vanilla `ssh host` after successful auth: `channel 0: open failed:
|
||||||
administratively prohibited: Administratively prohibited`, exit 255 —
|
administratively prohibited: Administratively prohibited`, exit 255:
|
||||||
clean diagnosis, no hang, and the **connection-level auth remains
|
clean diagnosis, no hang, and the **connection-level auth remains
|
||||||
intact** (the decline is channel-scoped, not a disconnect).
|
intact** (the decline is channel-scoped, not a disconnect).
|
||||||
- Vanilla `ssh -L 127.0.0.1:P:127.0.0.1:P -N`: the local listener
|
- Vanilla `ssh -L 127.0.0.1:P:127.0.0.1:P -N`: the local listener
|
||||||
establishes; dialing through it opens a `direct-tcpip` channel that
|
establishes; dialing through it opens a `direct-tcpip` channel that
|
||||||
is refused — the local dial gets TCP-level reset ("Connection reset
|
is refused, so the local dial gets TCP-level reset ("Connection reset
|
||||||
by peer"), no data ever crosses. The server-side log shows the drop
|
by peer"), no data ever crosses. The server-side log shows the drop
|
||||||
with the forward target.
|
with the forward target.
|
||||||
- Session remains usable after declines (further channel opens
|
- Session remains usable after declines (further channel opens
|
||||||
correctly declined too — no state poisoning).
|
correctly declined too, with no state poisoning).
|
||||||
|
|
||||||
### 6. russh client API shapes on rejects (new facts for the mapping docs)
|
### 6. russh client API shapes on rejects (new facts for the mapping docs)
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ signed declined attempts and was disconnected with
|
|||||||
auto-reject covers x11, direct-tcpip, forwarded-tcpip,
|
auto-reject covers x11, direct-tcpip, forwarded-tcpip,
|
||||||
direct-streamlocal uniformly.
|
direct-streamlocal uniformly.
|
||||||
- `PublicKeyOrCertificate::PublicKey` is a struct variant
|
- `PublicKeyOrCertificate::PublicKey` is a struct variant
|
||||||
(`{ key, hash_alg }`) — small API detail when reading host keys in
|
(`{ key, hash_alg }`), a small API detail when reading host keys in
|
||||||
`check_server_key`.
|
`check_server_key`.
|
||||||
- **Client-side plumb caveat (recorded for POC-2/3):** `Channel::wait()`
|
- **Client-side plumb caveat (recorded for POC-2/3):** `Channel::wait()`
|
||||||
on a just-opened client channel *before* `into_stream()` breaks the
|
on a just-opened client channel *before* `into_stream()` breaks the
|
||||||
@@ -142,7 +142,7 @@ signed declined attempts and was disconnected with
|
|||||||
internally; do not consume the channel queue before converting to a
|
internally; do not consume the channel queue before converting to a
|
||||||
stream. With that caveat respected, the end-to-end echo works.
|
stream. With that caveat respected, the end-to-end echo works.
|
||||||
|
|
||||||
### 7. Channel ↔ AsyncRead/AsyncWrite plumbing — confirmed
|
### 7. Channel ↔ AsyncRead/AsyncWrite plumbing (confirmed)
|
||||||
|
|
||||||
`into_stream()` works on both sides: server accepts a session channel,
|
`into_stream()` works on both sides: server accepts a session channel,
|
||||||
`channel.into_stream()`, reads/writes in a plain
|
`channel.into_stream()`, reads/writes in a plain
|
||||||
@@ -154,17 +154,17 @@ POC-2 (dial lifecycle) and POC-3 (mapping bake-off) will reuse.
|
|||||||
## Hypothesis verdicts (pocs.md §POC-1)
|
## Hypothesis verdicts (pocs.md §POC-1)
|
||||||
|
|
||||||
- "`auth_publickey` → fingerprint string parity → resolve → accept;
|
- "`auth_publickey` → fingerprint string parity → resolve → accept;
|
||||||
unknown key → clean decline" — **confirmed**.
|
unknown key → clean decline" is **confirmed**.
|
||||||
- "`auth_publickey_offered` accept-and-let-decide with OpenSSH
|
- "`auth_publickey_offered` accept-and-let-decide with OpenSSH
|
||||||
probe-then-sign" — **confirmed** (and confirmed it doesn't inflate
|
probe-then-sign" is **confirmed** (and confirmed it doesn't inflate
|
||||||
the attempt counter).
|
the attempt counter).
|
||||||
- "Default auth impls are accept — every default must be overridden"
|
- "Default auth impls are accept, so every default must be overridden"
|
||||||
— **corrected**: at 0.64.1 only the offered hook defaults to accept.
|
is **corrected**: at 0.64.1 only the offered hook defaults to accept.
|
||||||
Explicit implementations still required by the family contract and
|
Explicit implementations still required by the family contract and
|
||||||
proven here.
|
proven here.
|
||||||
- "`ChannelOpenHandle` drop = auto-reject; declines observable by
|
- "`ChannelOpenHandle` drop = auto-reject; declines observable by
|
||||||
vanilla ssh; clean diagnostics" — **confirmed**.
|
vanilla ssh; clean diagnostics" is **confirmed**.
|
||||||
- "`Channel` ↔ AsyncRead/AsyncWrite (`into_stream`) works" —
|
- "`Channel` ↔ AsyncRead/AsyncWrite (`into_stream`) works" is
|
||||||
**confirmed**, with the wait-before-into_stream caveat.
|
**confirmed**, with the wait-before-into_stream caveat.
|
||||||
|
|
||||||
## OQ seeds carried forward
|
## OQ seeds carried forward
|
||||||
@@ -174,12 +174,12 @@ POC-2 (dial lifecycle) and POC-3 (mapping bake-off) will reuse.
|
|||||||
whether usernames are ever logged/matched (vanilla clients always
|
whether usernames are ever logged/matched (vanilla clients always
|
||||||
send one).
|
send one).
|
||||||
2. `max_auth_attempts` sizing must account for the counting `none`
|
2. `max_auth_attempts` sizing must account for the counting `none`
|
||||||
probe (see §4) — a config default note for the ADR.
|
probe (see §4), a config default note for the ADR.
|
||||||
3. Vanilla clients probe with `none` first; a future russh bump that
|
3. Vanilla clients probe with `none` first; a future russh bump that
|
||||||
changes reject-defaults would be a silent-auth posture change —
|
changes reject-defaults would be a silent-auth posture change, which
|
||||||
reinforces recording the russh pin as a deliberate one-way door.
|
reinforces recording the russh pin as a deliberate one-way door.
|
||||||
4. `auth_openssh_certificate` override-decline exists but was not
|
4. `auth_openssh_certificate` override-decline exists but was not
|
||||||
exercised with a real certificate (no CA in the POC) — the
|
exercised with a real certificate (no CA in the POC), so the
|
||||||
"decline initially" row stands unfalsified.
|
"decline initially" row stands unfalsified.
|
||||||
|
|
||||||
## Reference run
|
## Reference run
|
||||||
@@ -187,13 +187,13 @@ POC-2 (dial lifecycle) and POC-3 (mapping bake-off) will reuse.
|
|||||||
Full output of a clean run at `pocs.md` commit time:
|
Full output of a clean run at `pocs.md` commit time:
|
||||||
31 checks, 6 groups (fingerprint parity 2, russh client auth 9,
|
31 checks, 6 groups (fingerprint parity 2, russh client auth 9,
|
||||||
channel declines 3, vanilla OpenSSH 11, max_auth_attempts 1, echo
|
channel declines 3, vanilla OpenSSH 11, max_auth_attempts 1, echo
|
||||||
plumbing 5) — all PASS. Vanilla diagnostic strings quoted above are
|
plumbing 5), all PASS. Vanilla diagnostic strings quoted above are
|
||||||
verbatim from that run.
|
verbatim from that run.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- `pocs.md` §POC-1 (plan), `auth-bridge.md` (design), `russh.md` (API
|
- `pocs.md` §POC-1 (plan), `auth-bridge.md` (design), `russh.md` (API
|
||||||
inventory — updated with the 0.64.1 default-impl correction)
|
inventory, updated with the 0.64.1 default-impl correction)
|
||||||
- POC source: `/workspace/alkssh-poc1` (single `src/main.rs` + stub
|
- POC source: `/workspace/alkssh-poc1` (single `src/main.rs` + stub
|
||||||
alkcall-shaped `IdentityProvider` in `authbridge` module)
|
alkcall-shaped `IdentityProvider` in `authbridge` module)
|
||||||
- russh 0.64.1 (crates.io, ring backend), ssh-key 0.7.0-rc.11
|
- russh 0.64.1 (crates.io, ring backend), ssh-key 0.7.0-rc.11
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# POC-2 findings — client-side at-rest key handling (vault-backed storage)
|
# POC-2 findings: client-side at-rest key handling (vault-backed storage)
|
||||||
|
|
||||||
Status: Complete (phase 0). POC source: `/workspace/alkssh-poc2`
|
Status: Complete (phase 0). POC source: `/workspace/alkssh-poc2`
|
||||||
(standalone crate, disposable — never merges). All hypotheses in
|
(standalone crate, disposable, never merges). All hypotheses in
|
||||||
`pocs.md` §POC-2 were confirmed; the doc below records the evidence,
|
`pocs.md` §POC-2 were confirmed; the doc below records the evidence,
|
||||||
the inner-format comparison, the Signer-seam dismissal (now with
|
the inner-format comparison, the Signer-seam dismissal (now with
|
||||||
runtime + source evidence instead of the earlier reasoning-only
|
runtime + source evidence instead of the earlier reasoning-only
|
||||||
@@ -13,16 +13,16 @@ and the OQ seeds this POC produced.
|
|||||||
- russh **0.64.1** (crates.io, ring backend) + **alkvault 0.1.0**
|
- russh **0.64.1** (crates.io, ring backend) + **alkvault 0.1.0**
|
||||||
(crates.io; its pinned surface: `EncryptedData`, `VaultServiceHandle`,
|
(crates.io; its pinned surface: `EncryptedData`, `VaultServiceHandle`,
|
||||||
`CURRENT_KEY_VERSION`), ssh-key **0.7.0-rc.11** exact-pinned and
|
`CURRENT_KEY_VERSION`), ssh-key **0.7.0-rc.11** exact-pinned and
|
||||||
re-exported through `russh::keys::ssh_key` (identical tree to POC-1's
|
re-exported through `russh::keys::ssh_key` (identical tree to POC-1's,
|
||||||
— the two POCs exercised the same dependency graph).
|
since the two POCs exercised the same dependency graph).
|
||||||
- Server side: POC-1's loopback shape verbatim (fingerprint registry
|
- Server side: POC-1's loopback shape verbatim (fingerprint registry
|
||||||
fed from `ssh-keygen -lf` output, stub `IdentityProvider`,
|
fed from `ssh-keygen -lf` output, stub `IdentityProvider`,
|
||||||
`into_stream` echo server, no `wait()` before `into_stream`).
|
`into_stream` echo server, no `wait()` before `into_stream`).
|
||||||
- The adapter-shaped seam under test (`vaultkey` module in the POC):
|
- The adapter-shaped seam under test (`vaultkey` module in the POC):
|
||||||
- `KeySource { blob: Option<EncryptedData> }` — the assembly-layer
|
- `KeySource { blob: Option<EncryptedData> }`, the assembly-layer
|
||||||
storage stand-in (`None` = absent material);
|
storage stand-in (`None` = absent material);
|
||||||
- `VaultKeyStore::load(&KeySource) -> Result<DialKey, LoadError>` —
|
- `VaultKeyStore::load(&KeySource) -> Result<DialKey, LoadError>`, the
|
||||||
decrypt-at-dial: `vault.decrypt(blob)` → plaintext wrapped in
|
decrypt-at-dial path: `vault.decrypt(blob)` → plaintext wrapped in
|
||||||
`Zeroizing<String>` at the seam → one-time
|
`Zeroizing<String>` at the seam → one-time
|
||||||
`russh::keys::decode_secret_key(&plain, None)` → fingerprint
|
`russh::keys::decode_secret_key(&plain, None)` → fingerprint
|
||||||
computed in the same step. Every error shape maps to a distinct
|
computed in the same step. Every error shape maps to a distinct
|
||||||
@@ -33,7 +33,7 @@ and the OQ seeds this POC produced.
|
|||||||
|
|
||||||
## Validated: all checks pass (39/39)
|
## Validated: all checks pass (39/39)
|
||||||
|
|
||||||
### 1. At-rest shape — OpenSSH-encoded key inside alkvault `EncryptedData` confirmed; the `&str`-only API constraint is decisive
|
### 1. At-rest shape: OpenSSH-encoded key inside alkvault `EncryptedData` confirmed; the `&str`-only API constraint is decisive
|
||||||
|
|
||||||
alkvault's `encrypt`/`decrypt` surface is **`&str` in, `String` out**
|
alkvault's `encrypt`/`decrypt` surface is **`&str` in, `String` out**
|
||||||
(`VaultServiceHandle::encrypt(plaintext: &str, key_version: u32) ->
|
(`VaultServiceHandle::encrypt(plaintext: &str, key_version: u32) ->
|
||||||
@@ -44,9 +44,9 @@ dialed:
|
|||||||
|
|
||||||
| inner format | plaintext | blob ciphertext | key reconstruction at use | verdict |
|
| inner format | plaintext | blob ciphertext | key reconstruction at use | verdict |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| **OpenSSH PEM** (unencrypted) | 387 B | 540 B | `decode_secret_key(plain, None)` — one call | **winner** |
|
| **OpenSSH PEM** (unencrypted) | 387 B | 540 B | `decode_secret_key(plain, None)`, one call | **winner** |
|
||||||
| raw ed25519 seed (base64) | 32 B | 80 B | `Ed25519Keypair::from_seed(&seed) -> PrivateKey` (`From` exists) | works, smallest blob; drops comment/public metadata (pub is re-derived from seed — fine) |
|
| raw ed25519 seed (base64) | 32 B | 80 B | `Ed25519Keypair::from_seed(&seed) -> PrivateKey` (`From` exists) | works, smallest blob; drops comment/public metadata (pub is re-derived from seed, which is fine) |
|
||||||
| ssh-key self-encrypted PEM (passphrase, bcrypt KDF) | 449 B | 616 B | two decryptions: vault, then `decode_secret_key(pem, Some(pass))` | works but pointless — the passphrase is *itself* a stored secret, so the model just moves the same problem one level down; double-wrapping buys nothing |
|
| ssh-key self-encrypted PEM (passphrase, bcrypt KDF) | 449 B | 616 B | two decryptions: vault, then `decode_secret_key(pem, Some(pass))` | works but pointless: the passphrase is *itself* a stored secret, so the model just moves the same problem one level down; double-wrapping buys nothing |
|
||||||
| PKCS#8 PEM (unencrypted) | 246 B | 252 B | `encode_pkcs8_pem` (russh) at rest; `decode_secret_key(plain, None)` at use | works; loses the native comment/model round-trip; adds nothing over OpenSSH PEM |
|
| PKCS#8 PEM (unencrypted) | 246 B | 252 B | `encode_pkcs8_pem` (russh) at rest; `decode_secret_key(plain, None)` at use | works; loses the native comment/model round-trip; adds nothing over OpenSSH PEM |
|
||||||
|
|
||||||
(All four rows produced the correct fingerprint through the full
|
(All four rows produced the correct fingerprint through the full
|
||||||
@@ -59,7 +59,7 @@ The **OpenSSH PEM inner wins** on honest grounds: ASCII natively (no
|
|||||||
encode layer), self-describing (algorithm + public key + comment
|
encode layer), self-describing (algorithm + public key + comment
|
||||||
embedded), vanilla-OpenSSH-tool-compatible at every boundary, and
|
embedded), vanilla-OpenSSH-tool-compatible at every boundary, and
|
||||||
exactly one decode call from russh. The seed variant is a legitimate
|
exactly one decode call from russh. The seed variant is a legitimate
|
||||||
size optimization the ADR can note (80 B vs 540 B blobs) — it couples
|
size optimization the ADR can note (80 B vs 540 B blobs), but it couples
|
||||||
alkssh's storage to a fixed key shape though, whereas "a PEM file's
|
alkssh's storage to a fixed key shape though, whereas "a PEM file's
|
||||||
bytes" works for every algorithm ssh-key can decode. This mirrors the
|
bytes" works for every algorithm ssh-key can decode. This mirrors the
|
||||||
alkhttp ADR-014 analogy exactly as hypothesized in `pocs.md`.
|
alkhttp ADR-014 analogy exactly as hypothesized in `pocs.md`.
|
||||||
@@ -69,17 +69,17 @@ At-rest hygiene, verified end to end:
|
|||||||
- `EncryptedData { key_version: 2, salt, iv, data }` serializes to
|
- `EncryptedData { key_version: 2, salt, iv, data }` serializes to
|
||||||
stable JSON (serde), roundtrips, and decrypts to the byte-identical
|
stable JSON (serde), roundtrips, and decrypts to the byte-identical
|
||||||
original PEM. The salt field is 32 B (44 b64 chars), the IV 12 B
|
original PEM. The salt field is 32 B (44 b64 chars), the IV 12 B
|
||||||
(16 b64 chars) — pure wire-format confirmation against the type's
|
(16 b64 chars), pure wire-format confirmation against the type's
|
||||||
documented shape.
|
documented shape.
|
||||||
- AES-256-GCM opacity: the ciphertext's `data` field contains no
|
- AES-256-GCM opacity: the ciphertext's `data` field contains no
|
||||||
recognizable substrings of the key (checked `OPENSSH`, `PRIVATE`,
|
recognizable substrings of the key (checked `OPENSSH`, `PRIVATE`,
|
||||||
`ssh-ed25519`, and a 30-byte interior slice of the PEM — none found
|
`ssh-ed25519`, and a 30-byte interior slice of the PEM; none found
|
||||||
in ciphertext or the serialized JSON).
|
in ciphertext or the serialized JSON).
|
||||||
- Fresh IV + fresh salt per encrypt: re-encrypting the identical
|
- Fresh IV + fresh salt per encrypt: re-encrypting the identical
|
||||||
plaintext produces entirely different blobs (no determinism trap for
|
plaintext produces entirely different blobs (no determinism trap for
|
||||||
naive dedup code to stumble into).
|
naive dedup code to stumble into).
|
||||||
|
|
||||||
### 2. Dial lifecycle — decrypt-at-dial, zeroization bounds, lock/unlock mid-session
|
### 2. Dial lifecycle: decrypt-at-dial, zeroization bounds, lock/unlock mid-session
|
||||||
|
|
||||||
- The full dial through the adapter works: `load` →
|
- The full dial through the adapter works: `load` →
|
||||||
`authenticate_publickey` → `Success`, the server's
|
`authenticate_publickey` → `Success`, the server's
|
||||||
@@ -95,10 +95,10 @@ At-rest hygiene, verified end to end:
|
|||||||
- Re-`unlock` with the same mnemonic → decrypt works again, same
|
- Re-`unlock` with the same mnemonic → decrypt works again, same
|
||||||
fingerprint. Lock/unlock cycles are side-effect-free on the key.
|
fingerprint. Lock/unlock cycles are side-effect-free on the key.
|
||||||
- **Zeroization bounds** (source-verified in ssh-key 0.7.0-rc.11):
|
- **Zeroization bounds** (source-verified in ssh-key 0.7.0-rc.11):
|
||||||
- `Ed25519PrivateKey` zeroizes on `Drop` (`private/ed25519.rs` —
|
- `Ed25519PrivateKey` zeroizes on `Drop` (`private/ed25519.rs`,
|
||||||
`impl Drop { self.0.zeroize() }`), so the decoded key's secret
|
`impl Drop { self.0.zeroize() }`), so the decoded key's secret
|
||||||
bytes are cleaned up by ssh-key itself when the `PrivateKey` (or
|
bytes are cleaned up by ssh-key itself when the `PrivateKey` (or
|
||||||
clones of it — the type is `Clone` with zeroizing storage) drops;
|
clones of it, the type is `Clone` with zeroizing storage) drops;
|
||||||
- alkvault zeroizes its own key material (`EncryptionKey`
|
- alkvault zeroizes its own key material (`EncryptionKey`
|
||||||
`#[zeroize(drop)]`, mnemonic phrase via `Zeroizing`, `lock()`
|
`#[zeroize(drop)]`, mnemonic phrase via `Zeroizing`, `lock()`
|
||||||
purges seed + cache);
|
purges seed + cache);
|
||||||
@@ -107,32 +107,32 @@ At-rest hygiene, verified end to end:
|
|||||||
from the vault's return until the adapter takes ownership. The
|
from the vault's return until the adapter takes ownership. The
|
||||||
adapter shape proven here wraps it in `Zeroizing<String>` at the
|
adapter shape proven here wraps it in `Zeroizing<String>` at the
|
||||||
seam (owning the move), so the adapter-bound bytes zeroize on
|
seam (owning the move), so the adapter-bound bytes zeroize on
|
||||||
drop — but the pre-wrap window is the vault's API shape to close.
|
drop, but the pre-wrap window is the vault's API shape to close.
|
||||||
→ OQ seed below.
|
→ OQ seed below.
|
||||||
|
|
||||||
### 3. Failure matrix — every failure mode observed; all fail closed
|
### 3. Failure matrix: every failure mode observed, all fail closed
|
||||||
|
|
||||||
| disturbed condition | observed | adapter mapping |
|
| disturbed condition | observed | adapter mapping |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| vault locked | `VaultServiceError::VaultLocked` | `LoadError::Locked` — no dial |
|
| vault locked | `VaultServiceError::VaultLocked` | `LoadError::Locked`; no dial |
|
||||||
| blob absent at source (assembly layer has no record) | no vault call needed | `LoadError::Absent` — no dial |
|
| blob absent at source (assembly layer has no record) | no vault call needed | `LoadError::Absent`; no dial |
|
||||||
| wrong mnemonic (unrelated seed) | `VaultServiceError::Encryption("decryption error: aead::Error")` — a *decryption* failure, distinguishable from `Locked` | no dial |
|
| wrong mnemonic (unrelated seed) | `VaultServiceError::Encryption("decryption error: aead::Error")`, a *decryption* failure, distinguishable from `Locked` | no dial |
|
||||||
| tampered ciphertext (one flipped byte) | GCM integrity failure (same decrypt-error family) | no dial |
|
| tampered ciphertext (one flipped byte) | GCM integrity failure (same decrypt-error family) | no dial |
|
||||||
| `key_version = 0` (reserved/legacy) | `VaultServiceError::InvalidPath` — the vault refuses before touching crypto | no dial |
|
| `key_version = 0` (reserved/legacy) | `VaultServiceError::InvalidPath`; the vault refuses before touching crypto | no dial |
|
||||||
| `key_version = 7` (future, undecodable) | derivation-path error | no dial |
|
| `key_version = 7` (future, undecodable) | derivation-path error | no dial |
|
||||||
| healthy rotation v2→v3 | `rotate()` → new blob stamped `key_version: 3`, decrypt yields the identical PEM, decoded fingerprint identical | dial succeeds |
|
| healthy rotation v2→v3 | `rotate()` → new blob stamped `key_version: 3`, decrypt yields the identical PEM, decoded fingerprint identical | dial succeeds |
|
||||||
|
|
||||||
The wrong-mnemonic/tampered cases land in
|
The wrong-mnemonic/tampered cases land in
|
||||||
`VaultServiceError::Encryption` *as strings* — the error taxonomy at
|
`VaultServiceError::Encryption` *as strings*, and the error taxonomy at
|
||||||
the adapter is ours to shape (distinct variants), but the root causes
|
the adapter is ours to shape (distinct variants), but the root causes
|
||||||
are distinguishable today via the variant + message. The `InvalidPath` behavior for version < 2 is deliberate (v1 is the TS_PBKDF2 legacy the
|
are distinguishable today via the variant + message. The `InvalidPath` behavior for version < 2 is deliberate (v1 is the TS_PBKDF2 legacy the
|
||||||
vault cannot decrypt; rotation re-encryption is the migration path,
|
vault cannot decrypt; rotation re-encryption is the migration path,
|
||||||
per alkvault's own docs) — consistent with alkvault's ADR-021
|
per alkvault's own docs), consistent with alkvault's ADR-021
|
||||||
version-indexed rotation model.
|
version-indexed rotation model.
|
||||||
|
|
||||||
### 4. Signer-seam dismissal — confirmed, now with runtime and source evidence
|
### 4. Signer-seam dismissal: confirmed, now with runtime and source evidence
|
||||||
|
|
||||||
`pocs.md` expected the answer "no — recorded so the ADR can dismiss it
|
`pocs.md` expected the answer "no, recorded so the ADR can dismiss it
|
||||||
with evidence". Evidence assembled:
|
with evidence". Evidence assembled:
|
||||||
|
|
||||||
- **The trait exists but has no provided implementation**: russh
|
- **The trait exists but has no provided implementation**: russh
|
||||||
@@ -141,50 +141,50 @@ with evidence". Evidence assembled:
|
|||||||
hash_alg, to_sign)`), and `Handle::authenticate_publickey_with(user,
|
hash_alg, to_sign)`), and `Handle::authenticate_publickey_with(user,
|
||||||
ssh_key::PublicKey, hash_alg, &mut S)` accepts it. The doc comment
|
ssh_key::PublicKey, hash_alg, &mut S)` accepts it. The doc comment
|
||||||
claims "Currently, this crate only provides an implementation for an
|
claims "Currently, this crate only provides an implementation for an
|
||||||
SSH agent" — **that is stale**: there is no `impl Signer` for
|
SSH agent", but **that is stale**: there is no `impl Signer` for
|
||||||
`AgentClient` (or anything else) anywhere in the 0.64.1 tree. A
|
`AgentClient` (or anything else) anywhere in the 0.64.1 tree. A
|
||||||
custom signer must be built by hand.
|
custom signer must be built by hand.
|
||||||
- **The wire contract is non-obvious and hand-rolled**: the flow is
|
- **The wire contract is non-obvious and hand-rolled**: the flow is
|
||||||
probe (`has_sig=0`, `write_auth_request`) → server `USERAUTH_PK_OK`
|
probe (`has_sig=0`, `write_auth_request`) → server `USERAUTH_PK_OK`
|
||||||
→ client builds `session_id || userauth-prefix` (`client_make_to_sign`)
|
→ client builds `session_id || userauth-prefix` (`client_make_to_sign`)
|
||||||
→ `Reply::SignRequest` carries the full buffer → the signer returns
|
→ `Reply::SignRequest` carries the full buffer → the signer returns
|
||||||
**`to_sign || string{signature}`** — the original buffer with the
|
**`to_sign || string{signature}`** (the original buffer with the
|
||||||
signature length-prefixed as an SSH string — and the client splices
|
signature length-prefixed as an SSH string) and the client splices
|
||||||
`buffer[i0..]` as the packet payload. (First attempt appended the raw
|
`buffer[i0..]` as the packet payload. (First attempt appended the raw
|
||||||
signature without the outer length prefix: the server silently
|
signature without the outer length prefix: the server silently
|
||||||
misparsed and declined — `USERAUTH_FAILURE` — *without the
|
misparsed and declined (`USERAUTH_FAILURE`), *without the
|
||||||
`auth_publickey` hook ever firing*. Signature-verification failures
|
`auth_publickey` hook ever firing*. Signature-verification failures
|
||||||
happen inside russh before any handler hook, which is also a clean
|
happen inside russh before any handler hook, which is also a clean
|
||||||
fail-closed property worth noting for the mapping docs.)
|
fail-closed property worth noting for the mapping docs.)
|
||||||
- **Runtime proof that the seam works anyway**: `VaultSigner`
|
- **Runtime proof that the seam works anyway**: `VaultSigner`
|
||||||
(decrypt-per-sign against the vault) authenticated successfully
|
(decrypt-per-sign against the vault) authenticated successfully
|
||||||
end-to-end via `authenticate_publickey_with` — the splice convention
|
end-to-end via `authenticate_publickey_with`: the splice convention
|
||||||
replicated, `PrivateKey: signature::Signer::try_sign` +
|
replicated, `PrivateKey: signature::Signer::try_sign` +
|
||||||
`.encoded()` (i.e. `sig` → SSH wire blob) then length-prefixed into
|
`.encoded()` (i.e. `sig` → SSH wire blob) then length-prefixed into
|
||||||
the returned buffer.
|
the returned buffer.
|
||||||
- **Locked-vault fail-closed on the Signer path, too**: with the vault
|
- **Locked-vault fail-closed on the Signer path, too**: with the vault
|
||||||
locked, the sign-time decrypt fails and the auth call itself errors
|
locked, the sign-time decrypt fails and the auth call itself errors
|
||||||
(`Err` from `authenticate_publickey_with`) — no half-auth, no
|
(`Err` from `authenticate_publickey_with`): no half-auth, no
|
||||||
fallback.
|
fallback.
|
||||||
- **Verdict: dismissed for the vault path.** The vault is in-process:
|
- **Verdict: dismissed for the vault path.** The vault is in-process:
|
||||||
decrypted key bytes necessarily exist client-side at *either* dial
|
decrypted key bytes necessarily exist client-side at *either* dial
|
||||||
time or sign time — the Signer seam buys no custody improvement, and
|
time or sign time; the Signer seam buys no custody improvement, and
|
||||||
decrypt-per-sign merely multiplies GCM+parse work per signature while
|
decrypt-per-sign merely multiplies GCM+parse work per signature while
|
||||||
keeping *more* plaintext copies alive (per-signature) rather than
|
keeping *more* plaintext copies alive (per-signature) rather than
|
||||||
fewer. The seam's genuine home is *external* signer processes
|
fewer. The seam's genuine home is *external* signer processes
|
||||||
(ssh-agent), which the family treats as out of scope. The ADR can
|
(ssh-agent), which the family treats as out of scope. The ADR can
|
||||||
record: one-time `decode_secret_key` at dial; `Signer` not taken.
|
record: one-time `decode_secret_key` at dial; `Signer` not taken.
|
||||||
|
|
||||||
### 5. Host keys ride the same model — server bootstrap + vanilla compatibility
|
### 5. Host keys ride the same model: server bootstrap + vanilla compatibility
|
||||||
|
|
||||||
- The server's host key was generated in-POC, vault-encrypted, and
|
- The server's host key was generated in-POC, vault-encrypted, and
|
||||||
decrypted-at-construction into `Config::keys` through the same
|
decrypted-at-construction into `Config::keys` through the same
|
||||||
`VaultKeyStore::load` the client dial uses — one model, both sides.
|
`VaultKeyStore::load` the client dial uses: one model, both sides.
|
||||||
Vanilla OpenSSH authenticated against that server, the
|
Vanilla OpenSSH authenticated against that server, the
|
||||||
`known_hosts` fingerprint (written by the vanilla client) matched
|
`known_hosts` fingerprint (written by the vanilla client) matched
|
||||||
the vault host key's fingerprint exactly, and the russh client's
|
the vault host key's fingerprint exactly, and the russh client's
|
||||||
`check_server_key` observed the same fingerprint.
|
`check_server_key` observed the same fingerprint.
|
||||||
- The client's *pin* — the expected host fingerprint — is **public
|
- The client's *pin* (the expected host fingerprint) is **public
|
||||||
config, not vault material** (`check_server_key` policy + the
|
config, not vault material** (`check_server_key` policy + the
|
||||||
known-hosts entry); only the server's *private* host key is held
|
known-hosts entry); only the server's *private* host key is held
|
||||||
secret. The ADR's "assembly configures `Config::keys`" note can now
|
secret. The ADR's "assembly configures `Config::keys`" note can now
|
||||||
@@ -192,39 +192,39 @@ with evidence". Evidence assembled:
|
|||||||
- **Vanilla-tool compatibility of the plaintext**: the decrypted
|
- **Vanilla-tool compatibility of the plaintext**: the decrypted
|
||||||
client-key PEM written to a 0600 temp file was accepted by vanilla
|
client-key PEM written to a 0600 temp file was accepted by vanilla
|
||||||
`ssh -i` directly (auth succeeded; `Permission denied` never
|
`ssh -i` directly (auth succeeded; `Permission denied` never
|
||||||
appeared — exit 255 only because the POC server intentionally serves
|
appeared, with exit 255 only because the POC server intentionally serves
|
||||||
no exec). The at-rest bytes are, at rest *and* in motion, plain
|
no exec). The at-rest bytes are, at rest *and* in motion, plain
|
||||||
OpenSSH key material.
|
OpenSSH key material.
|
||||||
- Locked vault at server bootstrap → `LoadError::Locked` → **no
|
- Locked vault at server bootstrap → `LoadError::Locked` → **no
|
||||||
server is constructed** — the fail-closed posture extends to the
|
server is constructed**; the fail-closed posture extends to the
|
||||||
host-key boot path.
|
host-key boot path.
|
||||||
|
|
||||||
## Hypothesis verdicts (pocs.md §POC-2)
|
## Hypothesis verdicts (pocs.md §POC-2)
|
||||||
|
|
||||||
- "OpenSSH private-key encoding as the inner format; alkvault
|
- "OpenSSH private-key encoding as the inner format; alkvault
|
||||||
`encrypt`/`decrypt` (`EncryptedData`) as the outer wrapper" —
|
`encrypt`/`decrypt` (`EncryptedData`) as the outer wrapper" is
|
||||||
**confirmed**, and strengthened by the 4-way comparison table (the
|
**confirmed**, and strengthened by the 4-way comparison table (the
|
||||||
seed-only variant as a size optimization; the ssh-key self-encrypted
|
seed-only variant as a size optimization; the ssh-key self-encrypted
|
||||||
variant recorded as a double-wrapping antipattern).
|
variant recorded as a double-wrapping antipattern).
|
||||||
- "decrypt-at-dial → one-time `decode_secret_key` → zeroized handling
|
- "decrypt-at-dial → one-time `decode_secret_key` → zeroized handling
|
||||||
for the dial's lifetime" — **confirmed** (ssh-key zeroizes key bytes
|
for the dial's lifetime" is **confirmed** (ssh-key zeroizes key bytes
|
||||||
on drop; adapter owns plaintext zeroization at the seam; vault
|
on drop; adapter owns plaintext zeroization at the seam; vault
|
||||||
in-memory state self-purges on `lock`).
|
in-memory state self-purges on `lock`).
|
||||||
- "what happens locked / absent / rotation" — **confirmed as
|
- "what happens locked / absent / rotation" is **confirmed as
|
||||||
fail-closed** with distinct, mappable error shapes; healthy rotation
|
fail-closed** with distinct, mappable error shapes; healthy rotation
|
||||||
(v2→v3) preserves key identity.
|
(v2→v3) preserves key identity.
|
||||||
- "Signer-shaped seam still worth anything — expected: no" —
|
- "Signer-shaped seam still worth anything, expected answer: no" is
|
||||||
**dismissed with evidence** (no provided impl; hand-rolled splice
|
**dismissed with evidence** (no provided impl; hand-rolled splice
|
||||||
convention; in-process vault gains nothing; runtime proof the seam
|
convention; in-process vault gains nothing; runtime proof the seam
|
||||||
works anyway).
|
works anyway).
|
||||||
- "Server and client host-key material riding the same model" —
|
- "Server and client host-key material riding the same model" is
|
||||||
**confirmed** end to end, including vanilla-tool compatibility and
|
**confirmed** end to end, including vanilla-tool compatibility and
|
||||||
locked-at-bootstrap fail-closed.
|
locked-at-bootstrap fail-closed.
|
||||||
|
|
||||||
## API facts for the ADRs (all verified against crates.io sources)
|
## API facts for the ADRs (all verified against crates.io sources)
|
||||||
|
|
||||||
- **alkvault 0.1.0**: `VaultServiceHandle` is a sync, cloneable handle
|
- **alkvault 0.1.0**: `VaultServiceHandle` is a sync, cloneable handle
|
||||||
(interior `Arc<RwLock>`; short-held locks — no tokio coupling);
|
(interior `Arc<RwLock>`; short-held locks, no tokio coupling);
|
||||||
`unlock_new(word_count)` returns the phrase as `Zeroizing<String>`;
|
`unlock_new(word_count)` returns the phrase as `Zeroizing<String>`;
|
||||||
`encrypt(&str, u32)` / `decrypt(&EncryptedData) -> Result<String>` /
|
`encrypt(&str, u32)` / `decrypt(&EncryptedData) -> Result<String>` /
|
||||||
`rotate(&EncryptedData, to_version)`; current version 2, versions
|
`rotate(&EncryptedData, to_version)`; current version 2, versions
|
||||||
@@ -235,16 +235,16 @@ with evidence". Evidence assembled:
|
|||||||
`ssh-key/encryption` feature), PuTTY PPK, PKCS#5 and PKCS#8 (+ the
|
`ssh-key/encryption` feature), PuTTY PPK, PKCS#5 and PKCS#8 (+ the
|
||||||
`BEGIN PRIVATE KEY` / `BEGIN ENCRYPTED PRIVATE KEY` PEM cases);
|
`BEGIN PRIVATE KEY` / `BEGIN ENCRYPTED PRIVATE KEY` PEM cases);
|
||||||
`keys::encode_pkcs8_pem[_encrypted]` exist for writers (PKCS#8
|
`keys::encode_pkcs8_pem[_encrypted]` exist for writers (PKCS#8
|
||||||
support lives in *russh*, not in ssh-key — ssh-key rc11 has no
|
support lives in *russh*, not in ssh-key, which at rc11 has no
|
||||||
`pkcs8` feature; the research note about ssh-key-own-PKCS#8 does not
|
`pkcs8` feature; the research note about ssh-key-own-PKCS#8 does not
|
||||||
apply at this pin). `PrivateKey: signature::Signer<Signature>`,
|
apply at this pin). `PrivateKey: signature::Signer<Signature>`,
|
||||||
`Signature::encoded()` via `signature::SignatureEncoding`; russh's
|
`Signature::encoded()` via `signature::SignatureEncoding`; russh's
|
||||||
own signing (`helpers::sign_with_hash_alg`) is exactly
|
own signing (`helpers::sign_with_hash_alg`) is exactly
|
||||||
`try_sign(keypair, msg)?.encoded()` — a vault signer can mirror it,
|
`try_sign(keypair, msg)?.encoded()`, which a vault signer can mirror,
|
||||||
but `helpers` (and its `EncodedExt`) is crate-private; the public
|
but `helpers` (and its `EncodedExt`) is crate-private; the public
|
||||||
path is `russh::keys::signature::Signer` + `russh::keys::ssh_encoding::Encode`.
|
path is `russh::keys::signature::Signer` + `russh::keys::ssh_encoding::Encode`.
|
||||||
- **Server-side auth pipeline**: a failed/malformed signature produces
|
- **Server-side auth pipeline**: a failed/malformed signature produces
|
||||||
`USERAUTH_FAILURE` *without* calling `auth_publickey` — handler
|
`USERAUTH_FAILURE` *without* calling `auth_publickey`, so handler
|
||||||
hooks only fire on cryptographically valid signatures (POC-1
|
hooks only fire on cryptographically valid signatures (POC-1
|
||||||
established the "after verification" half; POC-2 adds "invalid sigs
|
established the "after verification" half; POC-2 adds "invalid sigs
|
||||||
never reach the handler at all").
|
never reach the handler at all").
|
||||||
@@ -255,7 +255,7 @@ with evidence". Evidence assembled:
|
|||||||
|
|
||||||
## OQ seeds carried forward
|
## OQ seeds carried forward
|
||||||
|
|
||||||
1. **alkvault `decrypt` returns plain `String`** — a zeroization gap at
|
1. **alkvault `decrypt` returns plain `String`**: a zeroization gap at
|
||||||
the seam (the vault's own key material is zeroized; the decrypted
|
the seam (the vault's own key material is zeroized; the decrypted
|
||||||
*plaintext* is not until the adapter wraps it). Candidate fixes:
|
*plaintext* is not until the adapter wraps it). Candidate fixes:
|
||||||
alkvault returns `Zeroizing<String>`, or the alkssh ADR documents
|
alkvault returns `Zeroizing<String>`, or the alkssh ADR documents
|
||||||
@@ -267,17 +267,17 @@ with evidence". Evidence assembled:
|
|||||||
`InvalidPath`
|
`InvalidPath`
|
||||||
for unknown
|
for unknown
|
||||||
versions is the fail-closed behavior). Rotation (at-rest version
|
versions is the fail-closed behavior). Rotation (at-rest version
|
||||||
bump, key constant) is a storage-side operation — proven to
|
bump, key constant) is a storage-side operation; proven to
|
||||||
preserve key identity (consistent with alkvault's own
|
preserve key identity (consistent with alkvault's own
|
||||||
version-indexed rotation decision, alkvault ADR-021).
|
version-indexed rotation decision, alkvault ADR-021).
|
||||||
3. **One vault, many roles?** The POC held client dial keys *and* the
|
3. **One vault, many roles?** The POC held client dial keys *and* the
|
||||||
server host key in one vault (one mnemonic). Whether the assembly
|
server host key in one vault (one mnemonic). Whether the assembly
|
||||||
layer should run separate vault instances per role (client vs
|
layer should run separate vault instances per role (client vs
|
||||||
server custody) is an assembly policy question, not an alkssh
|
server custody) is an assembly policy question, not an alkssh
|
||||||
design constraint — recorded so phase 1 doesn't bake it in.
|
design constraint, recorded so phase 1 doesn't bake it in.
|
||||||
4. **Never persist decrypted plaintext**: the vanilla-tool check
|
4. **Never persist decrypted plaintext**: the vanilla-tool check
|
||||||
wrote a decrypted key to a 0600 temp file (then zeroized and
|
wrote a decrypted key to a 0600 temp file (then zeroized and
|
||||||
deleted it) — that is a test-only demonstration of the byte format.
|
deleted it); that is a test-only demonstration of the byte format.
|
||||||
Adapters do not spill plaintext to disk; the vanilla-compatible PEM
|
Adapters do not spill plaintext to disk; the vanilla-compatible PEM
|
||||||
is handed to processes only via the assembly layer if ever.
|
is handed to processes only via the assembly layer if ever.
|
||||||
|
|
||||||
@@ -285,13 +285,13 @@ for unknown
|
|||||||
|
|
||||||
Full output of a clean run at findings time: **39 checks, 5 groups**
|
Full output of a clean run at findings time: **39 checks, 5 groups**
|
||||||
(at-rest shape 11, dial lifecycle 8, failure matrix 10, Signer seam 3,
|
(at-rest shape 11, dial lifecycle 8, failure matrix 10, Signer seam 3,
|
||||||
host key + vanilla 7) — all PASS. `overall=true`, exit 0.
|
host key + vanilla 7), all PASS. `overall=true`, exit 0.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- `pocs.md` §POC-2 (plan), `auth-bridge.md` §The client side (design —
|
- `pocs.md` §POC-2 (plan), `auth-bridge.md` §The client side (design,
|
||||||
updated with a pointer to these verdicts), `russh.md`
|
updated with a pointer to these verdicts), `russh.md`
|
||||||
(`client::Handle` authenticate methods — updated with the
|
(`client::Handle` authenticate methods, updated with the
|
||||||
Signer-verdict pointer)
|
Signer-verdict pointer)
|
||||||
- POC-1: `poc-1-auth-bridge.md` + `/workspace/alkssh-poc1`
|
- POC-1: `poc-1-auth-bridge.md` + `/workspace/alkssh-poc1`
|
||||||
(server loopback shape reused)
|
(server loopback shape reused)
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
# POC-3 findings — the mapping bake-off: channel bridge through real sibling seams
|
# POC-3 findings: the mapping bake-off, channel bridge through real sibling seams
|
||||||
|
|
||||||
Status: Complete (phase 0). POC source: `/workspace/alkssh-poc3`
|
Status: Complete (phase 0). POC source: `/workspace/alkssh-poc3`
|
||||||
(standalone crate, disposable — never merges). All four stages ran
|
(standalone crate, disposable, never merges). All four stages ran
|
||||||
against **vanilla clients** (OpenSSH 9.6 `ssh -L`/`-R`, real `git`
|
against **vanilla clients** (OpenSSH 9.6 `ssh -L`/`-R`, real `git`
|
||||||
2.43, real `curl` 8.5) and the **published crates.io forms** of every
|
2.43, real `curl` 8.5) and the **published crates.io forms** of every
|
||||||
sibling — no path dependencies, no in-family forks. The doc records
|
sibling, with no path dependencies and no in-family forks. The doc records
|
||||||
the evidence: three corrections to `mapping-table.md`, the russh
|
the evidence: three corrections to `mapping-table.md`, the russh
|
||||||
0.64.1 runtime facts phase 1 must code around (the inline-dispatch
|
0.64.1 runtime facts phase 1 must code around (the inline-dispatch
|
||||||
rule), the identity/scope live check, and the OQ seeds.
|
rule), the identity/scope live check, and the OQ seeds.
|
||||||
|
|
||||||
## Setup exercised
|
## Setup exercised
|
||||||
|
|
||||||
- russh **0.64.1** (crates.io, ring backend — the same published tree
|
- russh **0.64.1** (crates.io, ring backend; the same published tree
|
||||||
POC-1/POC-2 pinned; re-verified still the newest release). Same
|
POC-1/POC-2 pinned; re-verified still the newest release). Same
|
||||||
dependency tree as both prior POCs across all four stages.
|
dependency tree as both prior POCs across all four stages.
|
||||||
- Sibling seams, published forms: **alkgit 0.1.0 +
|
- Sibling seams, published forms: **alkgit 0.1.0 +
|
||||||
alkgit-substrate 0.1.0** (the shipped backend composition, with
|
alkgit-substrate 0.1.0** (the shipped backend composition, with
|
||||||
`alkstore-mem` engine + `alkblobs::Store::mem_only()` object pool —
|
`alkstore-mem` engine + `alkblobs::Store::mem_only()` object pool,
|
||||||
the substrate's own mem-assembly shape, `SubstrateAssembly::mem`),
|
the substrate's own mem-assembly shape, `SubstrateAssembly::mem`),
|
||||||
**alksocks 0.3.1** (`local` feature — the shipped `tcp_dial`),
|
**alksocks 0.3.1** (`local` feature, the shipped `tcp_dial`),
|
||||||
**alktunnels 0.4.1** (`local` feature — the shipped `local_dial`),
|
**alktunnels 0.4.1** (`local` feature, the shipped `local_dial`),
|
||||||
**alktty 0.8.1** (`local` feature — the shipped `LocalTtyBackend`
|
**alktty 0.8.1** (`local` feature, the shipped `LocalTtyBackend`
|
||||||
over portable-pty), **alkcall 0.10.1** (channels + identity types).
|
over portable-pty), **alkcall 0.10.1** (channels + identity types).
|
||||||
- Servers per stage were built as **feature-shaped builds**: stage A's
|
- Servers per stage were built as **feature-shaped builds**: stage A's
|
||||||
server serves only git-parseable exec (a git-only build), stage B's
|
server serves only git-parseable exec (a git-only build), stage B's
|
||||||
@@ -33,7 +33,7 @@ rule), the identity/scope live check, and the OQ seeds.
|
|||||||
- POC mechanics: single binary, stage functions returning check
|
- POC mechanics: single binary, stage functions returning check
|
||||||
reports (POC-1's harness); full clean runs reproduced twice.
|
reports (POC-1's harness); full clean runs reproduced twice.
|
||||||
|
|
||||||
## Stage A — git exec mapping (24/24 PASS)
|
## Stage A: git exec mapping (24/24 PASS)
|
||||||
|
|
||||||
### 1. Vanilla git works through the seam with zero seam fixups
|
### 1. Vanilla git works through the seam with zero seam fixups
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ vanilla git (GIT_SSH_COMMAND) → OpenSSH → russh server
|
|||||||
Limits, registry, refs, packgen, ingest, false, pipeline)
|
Limits, registry, refs, packgen, ingest, false, pipeline)
|
||||||
```
|
```
|
||||||
|
|
||||||
with the **real alkgit-substrate mem composition** as the backend —
|
with the **real alkgit-substrate mem composition** as the backend:
|
||||||
registry/ref-rows/packgen/ingest, an in-memory alkstore engine, and an
|
registry/ref-rows/packgen/ingest, an in-memory alkstore engine, and an
|
||||||
in-memory object pool. The published alkgit seam needed no wrapper, no
|
in-memory object pool. The published alkgit seam needed no wrapper, no
|
||||||
adapter glue beyond the command parse, and no dialect nudges. The
|
adapter glue beyond the command parse, and no dialect nudges. The
|
||||||
@@ -56,9 +56,9 @@ ADR work) is confirmed.
|
|||||||
|
|
||||||
### 2. The SSH exec-command grammar (the alkssh-side parse)
|
### 2. The SSH exec-command grammar (the alkssh-side parse)
|
||||||
|
|
||||||
What real OpenSSH git actually sends: `git-upload-pack '/owner/demo'`
|
What real OpenSSH git actually sends: `git-upload-pack '/owner/demo'`,
|
||||||
— single-quoted repo path with a leading `/` (the parse strips quotes
|
a single-quoted repo path with a leading `/` (the parse strips quotes
|
||||||
and the leading slash; no `\0host=` rides the SSH exec — the
|
and the leading slash; no `\0host=` rides the SSH exec, so the
|
||||||
mapping-table's "same grammar as preamble.rs, without `\0host=`" is
|
mapping-table's "same grammar as preamble.rs, without `\0host=`" is
|
||||||
exactly right). The POC grammar table (all rows verified):
|
exactly right). The POC grammar table (all rows verified):
|
||||||
|
|
||||||
@@ -66,14 +66,14 @@ exactly right). The POC grammar table (all rows verified):
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `git-upload-pack '/r'` / `git-receive-pack '/r'` / `git-upload-pack /r` | git parse, dispatched |
|
| `git-upload-pack '/r'` / `git-receive-pack '/r'` / `git-upload-pack /r` | git parse, dispatched |
|
||||||
| `git-send-pack '/r'`, `git-upload-archive '/r'` | declined (unknown service, honest surface) |
|
| `git-send-pack '/r'`, `git-upload-archive '/r'` | declined (unknown service, honest surface) |
|
||||||
| `git-upload-pack` (service-only, no repo) | declined — SSH exec must carry the repo |
|
| `git-upload-pack` (service-only, no repo) | declined: SSH exec must carry the repo |
|
||||||
| `uptime` (non-git) | declined parse-then-fail, no resource alloc |
|
| `uptime` (non-git) | declined parse-then-fail, no resource alloc |
|
||||||
|
|
||||||
### 3. Subsystem mapping row — corrected at the detail level
|
### 3. Subsystem mapping row (corrected at the detail level)
|
||||||
|
|
||||||
`mapping-table.md` says git subsystem names "arrive as either" (exec
|
`mapping-table.md` says git subsystem names "arrive as either" (exec
|
||||||
or subsystem). On the SSH wire a subsystem request carries **only the
|
or subsystem). On the SSH wire a subsystem request carries **only the
|
||||||
name** — no repository is available to resolve. The POC proves the
|
name**, and no repository is available to resolve. The POC proves the
|
||||||
forced decline: `subsystem("git-upload-pack")` and `subsystem("sftp")`
|
forced decline: `subsystem("git-upload-pack")` and `subsystem("sftp")`
|
||||||
both fail cleanly with a channel failure, with the finding recorded:
|
both fail cleanly with a channel failure, with the finding recorded:
|
||||||
*over SSH, git always uses exec; a subsystem git request can never
|
*over SSH, git always uses exec; a subsystem git request can never
|
||||||
@@ -85,21 +85,21 @@ The mapping-table row needs the detail fix.
|
|||||||
- **Server handler callbacks run inline inside the session's packet
|
- **Server handler callbacks run inline inside the session's packet
|
||||||
loop.** Blocking inside `exec_request`/`pty_request`/`shell_request`
|
loop.** Blocking inside `exec_request`/`pty_request`/`shell_request`
|
||||||
(or any handler) suspends packet processing for the *entire
|
(or any handler) suspends packet processing for the *entire
|
||||||
connection* — the awaited channel data is only routed by that same
|
connection*: the awaited channel data is only routed by that same
|
||||||
loop, so an inline serve **deadlocks** (receive-pack pushes bytes
|
loop, so an inline serve **deadlocks** (receive-pack pushes bytes
|
||||||
immediately after the exec request). Confirmed by construction: the
|
immediately after the exec request). Confirmed by construction: the
|
||||||
serving entry is spawned and the handler returns immediately. This
|
serving entry is spawned and the handler returns immediately. This
|
||||||
is the binding implementation constraint for the alkssh adapter:
|
is the binding implementation constraint for the alkssh adapter:
|
||||||
**every serving path is `tokio::spawn` + handler returns fast**.
|
**every serving path is `tokio::spawn` + handler returns fast**.
|
||||||
- The spawned task gets exit reporting through
|
- The spawned task gets exit reporting through
|
||||||
**`Session::handle()`** — a cloneable server-side `Handle`
|
**`Session::handle()`**, a cloneable server-side `Handle`
|
||||||
carrying `data` / `extended_data` / `eof` / `close` /
|
carrying `data` / `extended_data` / `eof` / `close` /
|
||||||
`exit_status_request` / `exit_signal_request`. The exit-status
|
`exit_status_request` / `exit_signal_request`. The exit-status
|
||||||
mapping row is implementable exactly as the table prescribes: send
|
mapping row is implementable exactly as the table prescribes: send
|
||||||
`exit-status` before `close` from the serving task.
|
`exit-status` before `close` from the serving task.
|
||||||
- The accepted-but-held `Channel` can be stored in the handler and
|
- The accepted-but-held `Channel` can be stored in the handler and
|
||||||
taken later in the request callback (`channels.remove(&id)` →
|
taken later in the request callback (`channels.remove(&id)` →
|
||||||
`into_stream()` in the spawned task) — no `wait()` anywhere
|
`into_stream()` in the spawned task), with no `wait()` anywhere
|
||||||
(POC-1's caveat holds server-side too).
|
(POC-1's caveat holds server-side too).
|
||||||
|
|
||||||
### 5. Exit-status fidelity, denial fidelity, vanilla diagnostics
|
### 5. Exit-status fidelity, denial fidelity, vanilla diagnostics
|
||||||
@@ -109,7 +109,7 @@ The mapping-table row needs the detail fix.
|
|||||||
protocol byte* (no existence oracle) and vanilla git reports
|
protocol byte* (no existence oracle) and vanilla git reports
|
||||||
`fatal: remote error: access denied: owner/absent`, exit 128.
|
`fatal: remote error: access denied: owner/absent`, exit 128.
|
||||||
- A russh client exec against the absent repo observed
|
- A russh client exec against the absent repo observed
|
||||||
**`ExitStatus{1}`** after the denial band — the exit-status flow is
|
**`ExitStatus{1}`** after the denial band; the exit-status flow is
|
||||||
proven in both directions, including the failure leg.
|
proven in both directions, including the failure leg.
|
||||||
- The git-only build's decline matrix held under vanilla ssh:
|
- The git-only build's decline matrix held under vanilla ssh:
|
||||||
`ssh host uptime` failed with the parse-then-fail decline logged;
|
`ssh host uptime` failed with the parse-then-fail decline logged;
|
||||||
@@ -117,7 +117,7 @@ The mapping-table row needs the detail fix.
|
|||||||
replies (russh client observed `ChannelMsg::Failure`); the fail-
|
replies (russh client observed `ChannelMsg::Failure`); the fail-
|
||||||
closed feature matrix of the mapping table is live end to end.
|
closed feature matrix of the mapping table is live end to end.
|
||||||
|
|
||||||
## Stage B — SOCKS5-in-channel (6/6 PASS)
|
## Stage B: SOCKS5-in-channel (6/6 PASS)
|
||||||
|
|
||||||
### 1. `serve_socks5_over` works over a russh session channel
|
### 1. `serve_socks5_over` works over a russh session channel
|
||||||
|
|
||||||
@@ -129,32 +129,32 @@ Construction: server accepts the session channel, spawns the
|
|||||||
domain-form targets). Client side: a local bridge listener pumps each
|
domain-form targets). Client side: a local bridge listener pumps each
|
||||||
accepted TCP socket bidirectionally into one authenticated session
|
accepted TCP socket bidirectionally into one authenticated session
|
||||||
channel. **Real `curl --socks5-hostname` through the whole chain**
|
channel. **Real `curl --socks5-hostname` through the whole chain**
|
||||||
returned the endpoint body byte-identically — CONNECT over the SSH
|
returned the endpoint body byte-identically: CONNECT over the SSH
|
||||||
wire, dialed at the server through the family seam.
|
wire, dialed at the server through the family seam.
|
||||||
|
|
||||||
### 2. OpenSSH's own `-D` never speaks SOCKS on the wire — mapping row corrected
|
### 2. OpenSSH's own `-D` never speaks SOCKS on the wire (mapping row corrected)
|
||||||
|
|
||||||
This is a **mapping-table correction**: vanilla `ssh -D` performs the
|
This is a **mapping-table correction**: vanilla `ssh -D` performs the
|
||||||
SOCKS5 handshake *client-side* and then opens plain **`direct-tcpip`
|
SOCKS5 handshake *client-side* and then opens plain **`direct-tcpip`
|
||||||
channels** per connection. The `session` → SOCKS-in-channel row is
|
channels** per connection. The `session` → SOCKS-in-channel row is
|
||||||
therefore the family-native client shape (an embedder/alktty-style
|
therefore the family-native client shape (an embedder/alktty-style
|
||||||
client speaking SOCKS5 on the channel), not what a vanilla `ssh -D`
|
client speaking SOCKS5 on the channel), not what a vanilla `ssh -D`
|
||||||
emits — vanilla `-D` traffic takes the stage-C row (direct-tcpip →
|
emits; vanilla `-D` traffic takes the stage-C row (direct-tcpip →
|
||||||
egress). Both rows are real; the table should say which client shape
|
egress). Both rows are real; the table should say which client shape
|
||||||
each serves. (POC evidence: this stage's proxy uses the session
|
each serves. (POC evidence: this stage's proxy uses the session
|
||||||
channel; stage C's `-L`-style row serves forwarded and SOCKS-originated
|
channel; stage C's `-L`-style row serves forwarded and SOCKS-originated
|
||||||
direct-tcpip alike.)
|
direct-tcpip alike.)
|
||||||
|
|
||||||
### 3. The dispatch split is real — and needs a bounded detect window
|
### 3. The dispatch split is real and needs a bounded detect window
|
||||||
|
|
||||||
Parse-then-route (stage A) and SOCKS-detect (this stage) are the two
|
Parse-then-route (stage A) and SOCKS-detect (this stage) are the two
|
||||||
session-channel dispatch modes the mapping table predicted, and both
|
session-channel dispatch modes the mapping table predicted, and both
|
||||||
work. The detect runs in a spawned task with a bounded window (500 ms
|
work. The detect runs in a spawned task with a bounded window (500 ms
|
||||||
in the POC): the first byte decides the route; a silent channel is
|
in the POC): the first byte decides the route; a silent channel is
|
||||||
declined. Two consequences for the ADR: (a) the detect must not block
|
declined. Two consequences for the ADR: (a) the detect must not block
|
||||||
inline (the §A.4 deadlock rule applies — the sniffed byte arrives via
|
inline (the §A.4 deadlock rule applies: the sniffed byte arrives via
|
||||||
the same loop that awaits it); (b) the *detect window* is a design
|
the same loop that awaits it); (b) the *detect window* is a design
|
||||||
shape to pin — an OQ seed: whether the dispatch is a first-byte sniff,
|
shape to pin, an OQ seed: whether the dispatch is a first-byte sniff,
|
||||||
an in-band preamble, or an exec-request marker chosen by the assembly.
|
an in-band preamble, or an exec-request marker chosen by the assembly.
|
||||||
|
|
||||||
### 4. Refusals are RFC-faithful through the seam
|
### 4. Refusals are RFC-faithful through the seam
|
||||||
@@ -165,15 +165,15 @@ curl exits **97** (proxy-connection-not-allowed class) with no data
|
|||||||
crossing. Backpressure/throughput: **8 MiB through the session-channel
|
crossing. Backpressure/throughput: **8 MiB through the session-channel
|
||||||
proxy in ~585 ms** (in-process route; the 1 MiB channels-reassembly
|
proxy in ~585 ms** (in-process route; the 1 MiB channels-reassembly
|
||||||
leg is not on this path). Non-SOCKS first byte on a session channel →
|
leg is not on this path). Non-SOCKS first byte on a session channel →
|
||||||
declined, logged — no HTTP serving happens on the SOCKS route.
|
declined, logged; no HTTP serving happens on the SOCKS route.
|
||||||
|
|
||||||
## Stage C — direct-tcpip via alktunnels route 1 (8/8 PASS)
|
## Stage C: direct-tcpip via alktunnels route 1 (8/8 PASS)
|
||||||
|
|
||||||
### 1. The bridged route works end to end with the published crates
|
### 1. The bridged route works end to end with the published crates
|
||||||
|
|
||||||
Route 1 (adapter-side bridge) exercised as the mapping table's route
|
Route 1 (adapter-side bridge) exercised as the mapping table's route
|
||||||
note describes: the russh server holds a **real `ChannelClient`** into
|
note describes: the russh server holds a **real `ChannelClient`** into
|
||||||
a family fabric — an in-process `alk/channels` connection whose
|
a family fabric, an in-process `alk/channels` connection whose
|
||||||
serving side (the `ChannelsAdapter` accept end) registers
|
serving side (the `ChannelsAdapter` accept end) registers
|
||||||
`ChannelOperations` + `register_tunnel_direct_openable` with the
|
`ChannelOperations` + `register_tunnel_direct_openable` with the
|
||||||
shipped `alktunnels::local::local_dial()` injected. Per vanilla `ssh
|
shipped `alktunnels::local::local_dial()` injected. Per vanilla `ssh
|
||||||
@@ -188,27 +188,27 @@ channel_open_direct_tcpip → TunnelSession::open_direct(
|
|||||||
Vanilla `ssh -L` + `curl` to the bridged port returned the endpoint
|
Vanilla `ssh -L` + `curl` to the bridged port returned the endpoint
|
||||||
body byte-identically; **8 MiB through the bridge in ~333 ms**. The
|
body byte-identically; **8 MiB through the bridge in ~333 ms**. The
|
||||||
`direct_egress` precedent in alksocks' `composed` feature is the same
|
`direct_egress` precedent in alksocks' `composed` feature is the same
|
||||||
shape — the family precedent survives contact with the SSH door.
|
shape; the family precedent survives contact with the SSH door.
|
||||||
|
|
||||||
### 2. Failure shapes at both ends of the bridge
|
### 2. Failure shapes at both ends of the bridge
|
||||||
|
|
||||||
- Fabric open failure → the SSH channel is rejected **explicitly**
|
- Fabric open failure → the SSH channel is rejected **explicitly**
|
||||||
(`ChannelOpenHandle::reject(ConnectFailed)`); accept is possible
|
(`ChannelOpenHandle::reject(ConnectFailed)`); accept is possible
|
||||||
only after `open_direct` succeeds because `accept(mut self)`
|
only after `open_direct` succeeds because `accept(mut self)`
|
||||||
consumes the handle — so the handle moves into the spawned task.
|
consumes the handle, so the handle moves into the spawned task.
|
||||||
A vanilla client dials a dead target and gets a clean curl failure
|
A vanilla client dials a dead target and gets a clean curl failure
|
||||||
(exit 56) instead of a hang.
|
(exit 56) instead of a hang.
|
||||||
- The `-R` ingress direction is **declined** on this egress-only
|
- The `-R` ingress direction is **declined** on this egress-only
|
||||||
build: `Handler::tcpip_forward` returns false (the global request
|
build: `Handler::tcpip_forward` returns false (the global request
|
||||||
refuses), vanilla `ssh -R` dies with `remote port forwarding
|
refuses), vanilla `ssh -R` dies with `remote port forwarding
|
||||||
failed for listen port` (exit 255), and the event log records the
|
failed for listen port` (exit 255), and the event log records the
|
||||||
refusal — honest advertisement, gate-before-wire.
|
refusal: honest advertisement, gate-before-wire.
|
||||||
- Unfeatured session-channel opens drop (auto-reject) unchanged from
|
- Unfeatured session-channel opens drop (auto-reject) unchanged from
|
||||||
POC-1 on this and the other stage builds.
|
POC-1 on this and the other stage builds.
|
||||||
|
|
||||||
### 3. Identity/scope propagation observed live
|
### 3. Identity/scope propagation observed live
|
||||||
|
|
||||||
The direct establisher's identity witness recorded **`u-writer`** —
|
The direct establisher's identity witness recorded **`u-writer`**;
|
||||||
the SSH-authenticated identity rides the transport connection into
|
the SSH-authenticated identity rides the transport connection into
|
||||||
the channels layer (CF-005 propagation), and the establisher's
|
the channels layer (CF-005 propagation), and the establisher's
|
||||||
per-call auth (CF-006) sees it. The visible-surface invariant's
|
per-call auth (CF-006) sees it. The visible-surface invariant's
|
||||||
@@ -227,30 +227,30 @@ only real lift, and the alkforge assembly will build exactly this):
|
|||||||
transport; `ChannelClient::from_connection` on the alkssh side with
|
transport; `ChannelClient::from_connection` on the alkssh side with
|
||||||
the identity attached before dialing.
|
the identity attached before dialing.
|
||||||
|
|
||||||
## Stage D — session/pty mapping (13/13 PASS)
|
## Stage D: session/pty mapping (13/13 PASS)
|
||||||
|
|
||||||
### 1. The mapping is a translation bridge, not a pump — mapping-table note confirmed with a shape
|
### 1. The mapping is a translation bridge, not a pump (mapping-table note confirmed with a shape)
|
||||||
|
|
||||||
alktty's data plane is **chunk-framed in both directions**
|
alktty's data plane is **chunk-framed in both directions**
|
||||||
(STREAM_STDIN/STDOUT/STDERR/CTRL_IN/CTRL_OUT, 5-byte header + payload
|
(STREAM_STDIN/STDOUT/STDERR/CTRL_IN/CTRL_OUT, 5-byte header + payload;
|
||||||
— `alktty::wire`). The SSH mapping therefore wraps the channel with
|
`alktty::wire`). The SSH mapping therefore wraps the channel with
|
||||||
two small adapters: SSH data/EOF → stdin chunks (+ zero-length EOF
|
two small adapters: SSH data/EOF → stdin chunks (+ zero-length EOF
|
||||||
sentinel); alktty stdout chunks → SSH data, stderr chunks → SSH
|
sentinel); alktty stdout chunks → SSH data, stderr chunks → SSH
|
||||||
`extended_data(1)`, ctrl-out exit chunk → SSH `exit-status` request +
|
`extended_data(1)`, ctrl-out exit chunk → SSH `exit-status` request +
|
||||||
close. `drive_session_pre_negotiated(writer, reader, NegotiateRequest
|
close. `drive_session_pre_negotiated(writer, reader, NegotiateRequest
|
||||||
built locally, backends, ownership, identity)` — the exact producer
|
built locally, backends, ownership, identity)`, the exact producer
|
||||||
construct the mapping table names — then runs the real session. Proven
|
construct the mapping table names, then runs the real session. Proven
|
||||||
against a **real PTY** (`LocalTtyBackend` over portable-pty):
|
against a **real PTY** (`LocalTtyBackend` over portable-pty):
|
||||||
|
|
||||||
- `pty_request` args → `TerminalParamsWire` (`term`/cols/rows/
|
- `pty_request` args → `TerminalParamsWire` (`term`/cols/rows/
|
||||||
pixels; SSH modes → the reserved `{}` JSON — an alktty-side
|
pixels; SSH modes → the reserved `{}` JSON, an alktty-side
|
||||||
reservation, alktty OQ-44; modes are deferred there) — live: bash on
|
reservation, alktty OQ-44; modes are deferred there); live: bash on
|
||||||
the PTY answered `stty size` with the requested 24×80.
|
the PTY answered `stty size` with the requested 24×80.
|
||||||
- Keystrokes ride in, PTY echo and shell output ride out (real bash
|
- Keystrokes ride in, PTY echo and shell output ride out (real bash
|
||||||
prompt captured in the run output).
|
prompt captured in the run output).
|
||||||
- `window_change_request` → `ControlMessage::Resize` as a CTRL_IN
|
- `window_change_request` → `ControlMessage::Resize` as a CTRL_IN
|
||||||
chunk → the PTY resized: `stty size` flipped to `40 120` after the
|
chunk → the PTY resized: `stty size` flipped to `40 120` after the
|
||||||
client sent `window_change(120, 40)` — the mapping row proven
|
client sent `window_change(120, 40)`, the mapping row proven
|
||||||
behaviorally.
|
behaviorally.
|
||||||
- Pipe-mode `exec_request`: stdout rode SSH data, stderr rode SSH
|
- Pipe-mode `exec_request`: stdout rode SSH data, stderr rode SSH
|
||||||
`extended_data(1)` (client received both distinctly), exit
|
`extended_data(1)` (client received both distinctly), exit
|
||||||
@@ -259,11 +259,11 @@ against a **real PTY** (`LocalTtyBackend` over portable-pty):
|
|||||||
local backend produces.
|
local backend produces.
|
||||||
- `signal(TERM)` → `ControlMessage::Signal` CTRL_IN chunk → the
|
- `signal(TERM)` → `ControlMessage::Signal` CTRL_IN chunk → the
|
||||||
backend killed the session; the exit chunk carried the negative
|
backend killed the session; the exit chunk carried the negative
|
||||||
code and the SSH reply was an **`exit-signal`** request — the
|
code and the SSH reply was an **`exit-signal`** request, matching the
|
||||||
mapping-table's "only reachable in signal-termination cases" row,
|
mapping-table's "only reachable in signal-termination cases" row,
|
||||||
now with live wire evidence.
|
now with live wire evidence.
|
||||||
- The git-parseable exec declined in the session-only build
|
- The git-parseable exec declined in the session-only build
|
||||||
(parse-then-route: the git parse is the `git` feature's gate) —
|
(parse-then-route: the git parse is the `git` feature's gate);
|
||||||
the feature-split failure matrix holds in this build too.
|
the feature-split failure matrix holds in this build too.
|
||||||
|
|
||||||
### 2. Signal/exit fidelity over the alktty exit-code convention
|
### 2. Signal/exit fidelity over the alktty exit-code convention
|
||||||
@@ -272,14 +272,14 @@ The exit chunk is `{"type":"exit","code":N}` (alktty ADR-004, ported
|
|||||||
from alknet ADR-055). Findings of this stage, verified against the
|
from alknet ADR-055). Findings of this stage, verified against the
|
||||||
published sources:
|
published sources:
|
||||||
|
|
||||||
- **Pipe path — the negative code is the signal encoding.** Pipe-mode
|
- **Pipe path: the negative code is the signal encoding.** Pipe-mode
|
||||||
signal-terminated children arrive as the negative signal number
|
signal-terminated children arrive as the negative signal number
|
||||||
verbatim from std's `ExitStatusExt` (`pipe.rs::exit_code_from` maps
|
verbatim from std's `ExitStatusExt` (`pipe.rs::exit_code_from` maps
|
||||||
`ExitStatus::signal()` to `−sig`; the alknet ADR-055 convention).
|
`ExitStatus::signal()` to `−sig`; the alknet ADR-055 convention).
|
||||||
So SSH `exit-signal` needs no wire change: the adapter's check
|
So SSH `exit-signal` needs no wire change: the adapter's check
|
||||||
mapped `−15` → `exit-signal("TERM")` — the name derivation is a
|
mapped `−15` → `exit-signal("TERM")`; the name derivation is a
|
||||||
server-side number→name table lookup, not an alktty ask.
|
server-side number→name table lookup, not an alktty ask.
|
||||||
- **PTY path — the signal number does not survive the backend.**
|
- **PTY path: the signal number does not survive the backend.**
|
||||||
portable-pty 0.9's `ExitStatus` conversion records
|
portable-pty 0.9's `ExitStatus` conversion records
|
||||||
`code = status.code().unwrap_or(1)` → **1** for signal-terminated
|
`code = status.code().unwrap_or(1)` → **1** for signal-terminated
|
||||||
children, keeping the signal only in a separate humanized
|
children, keeping the signal only in a separate humanized
|
||||||
@@ -288,16 +288,16 @@ published sources:
|
|||||||
through portable-pty 0.9's trait surface). Alktty's own PTY signal
|
through portable-pty 0.9's trait surface). Alktty's own PTY signal
|
||||||
tests assert `code != 0`, never the encoding. Consequence: faithful
|
tests assert `code != 0`, never the encoding. Consequence: faithful
|
||||||
`exit-signal` for signal-killed PTY sessions needs the alktty
|
`exit-signal` for signal-killed PTY sessions needs the alktty
|
||||||
backend to conform to its own ADR's negative-code convention — an
|
backend to conform to its own ADR's negative-code convention,
|
||||||
alktty/portable-pty ask with tty/pty-grounded justification (upstream
|
an alktty/portable-pty ask with tty/pty-grounded justification (upstream
|
||||||
trackers; the alkssh mapping adapts automatically, no hedge needed).
|
trackers; the alkssh mapping adapts automatically, no hedge needed).
|
||||||
- **The `−1` sentinel** (backend wait failure) collides with a real
|
- **The `−1` sentinel** (backend wait failure) collides with a real
|
||||||
SIGHUP termination (signal 1) — documented in alktty ADR-004 §4,
|
SIGHUP termination (signal 1), documented in alktty ADR-004 §4,
|
||||||
with an additive `exit_error` control type as its recorded extension
|
with an additive `exit_error` control type as its recorded extension
|
||||||
seam. Alkssh renders `−1` as `exit-status(255)` — it fabricates no
|
seam. Alkssh renders `−1` as `exit-status(255)`, fabricating no
|
||||||
signal for a wait failure.
|
signal for a wait failure.
|
||||||
- **Pipe-mode signals are pid-only** (a SIGTERM to `sh -c "sleep 60"`
|
- **Pipe-mode signals are pid-only** (a SIGTERM to `sh -c "sleep 60"`
|
||||||
leaves the orphaned `sleep` holding the stdout pipe — no exit chunk
|
leaves the orphaned `sleep` holding the stdout pipe, with no exit chunk
|
||||||
until the whole group dies); PTY-mode signals reach the process
|
until the whole group dies); PTY-mode signals reach the process
|
||||||
group (correct terminal semantics). The signal row's fidelity
|
group (correct terminal semantics). The signal row's fidelity
|
||||||
depends on the tty/pipe mode split the table already notes.
|
depends on the tty/pipe mode split the table already notes.
|
||||||
@@ -306,38 +306,38 @@ published sources:
|
|||||||
|
|
||||||
- russh client has `Channel::exec()` (there is no `request_exec`);
|
- russh client has `Channel::exec()` (there is no `request_exec`);
|
||||||
pty/subsystem request *failures* surface as `ChannelMsg::Failure`
|
pty/subsystem request *failures* surface as `ChannelMsg::Failure`
|
||||||
via `wait()` — **not** as `Err` from the request call; the caller
|
via `wait()` (not as `Err` from the request call); the caller
|
||||||
must read the channel until Success/Failure.
|
must read the channel until Success/Failure.
|
||||||
- `window_change`/`signal` are fire-and-forget (no want-reply), and
|
- `window_change`/`signal` are fire-and-forget (no want-reply), and
|
||||||
must be sent **before** `into_stream()` (after the conversion the
|
must be sent **before** `into_stream()` (after the conversion the
|
||||||
Channel is consumed) — server-side they carry no SSH reply, so the
|
Channel is consumed); server-side they carry no SSH reply, so the
|
||||||
handler must not send one.
|
handler must not send one.
|
||||||
- The server handler loop dispatches `CHANNEL_SUCCESS`-style replies
|
- The server handler loop dispatches `CHANNEL_SUCCESS`-style replies
|
||||||
only through explicit handler calls; request-level failure replies
|
only through explicit handler calls; request-level failure replies
|
||||||
in stage A/D all used `Session::channel_failure`.
|
in stage A/D all used `Session::channel_failure`.
|
||||||
- The channel message loop client-side delivers interleaved
|
- The channel message loop client-side delivers interleaved
|
||||||
Data/ExtendedData/Success/ExitStatus on one queue — drain to the
|
Data/ExtendedData/Success/ExitStatus on one queue; drain to the
|
||||||
expected message, `continue` past the rest (the POC hit the
|
expected message, `continue` past the rest (the POC hit the
|
||||||
break-on-first-message trap twice; noted so the alkssh consumer
|
break-on-first-message trap twice; noted so the alkssh consumer
|
||||||
half doesn't).
|
half doesn't).
|
||||||
|
|
||||||
## Hypothesis verdicts (pocs.md §POC-3)
|
## Hypothesis verdicts (pocs.md §POC-3)
|
||||||
|
|
||||||
- "one-seam-per-mapping hypothesis" — **confirmed** across all four
|
- "one-seam-per-mapping hypothesis" is **confirmed** across all four
|
||||||
seams with zero seam fixups; the mapping glue is adapter-side
|
seams with zero seam fixups; the mapping glue is adapter-side
|
||||||
(command parse, chunk translation, handle plumbing).
|
(command parse, chunk translation, handle plumbing).
|
||||||
- "parse-then-route dispatch" — **confirmed** (stage A), with the
|
- "parse-then-route dispatch" is **confirmed** (stage A), with the
|
||||||
SOCKS-detect variant confirmed in stage B plus the bounded-window
|
SOCKS-detect variant confirmed in stage B plus the bounded-window
|
||||||
and spawn-not-block constraints.
|
and spawn-not-block constraints.
|
||||||
- "EOF/close/signal fidelity end to end" — **confirmed**: exec →
|
- "EOF/close/signal fidelity end to end" is **confirmed**: exec →
|
||||||
serve → exit-status (0 and 1 legs), exit-signal for signal
|
serve → exit-status (0 and 1 legs), exit-signal for signal
|
||||||
termination, close mapping, refusal diagnostics at both bridge ends.
|
termination, close mapping, refusal diagnostics at both bridge ends.
|
||||||
- "stderr semantics in pipe vs PTY mode" — **confirmed** exactly as
|
- "stderr semantics in pipe vs PTY mode" is **confirmed** exactly as
|
||||||
the table describes (separate extended_data in pipe; merged under PTY).
|
the table describes (separate extended_data in pipe; merged under PTY).
|
||||||
- "throughput sanity" — **confirmed** with margin: 8 MiB in ~0.3–0.6 s
|
- "throughput sanity" is **confirmed** with margin: 8 MiB in ~0.3–0.6 s
|
||||||
per bridged route.
|
per bridged route.
|
||||||
- "stage D may surface PTY-merge stderr surprises that reshape the
|
- "stage D may surface PTY-merge stderr surprises that reshape the
|
||||||
request-level mapping rows" — partially: no surprises in the
|
request-level mapping rows": partially. No surprises in the
|
||||||
PTY/pipe split, but the **signal/exit-fidelity details** and the
|
PTY/pipe split, but the **signal/exit-fidelity details** and the
|
||||||
**translation-bridge shape** (two adapters, not a pump) do reshape
|
**translation-bridge shape** (two adapters, not a pump) do reshape
|
||||||
the session row's implementation notes.
|
the session row's implementation notes.
|
||||||
@@ -345,7 +345,7 @@ published sources:
|
|||||||
## Corrections recorded against phase 0 research
|
## Corrections recorded against phase 0 research
|
||||||
|
|
||||||
1. `mapping-table.md` §channel-type rows: the git **subsystem** claim
|
1. `mapping-table.md` §channel-type rows: the git **subsystem** claim
|
||||||
("git-upload-pack etc. arrive as either") is wrong over SSH —
|
("git-upload-pack etc. arrive as either") is wrong over SSH:
|
||||||
subsystem cannot carry the repo id, so even a git service name must
|
subsystem cannot carry the repo id, so even a git service name must
|
||||||
decline. Git over SSH is exec-only.
|
decline. Git over SSH is exec-only.
|
||||||
2. `mapping-table.md`: `ssh -D` from a vanilla client does **not**
|
2. `mapping-table.md`: `ssh -D` from a vanilla client does **not**
|
||||||
@@ -356,19 +356,19 @@ published sources:
|
|||||||
alktty bridge is a **translation bridge** (chunk framing both
|
alktty bridge is a **translation bridge** (chunk framing both
|
||||||
directions), not a byte pump; plus the exit/signal fidelity
|
directions), not a byte pump; plus the exit/signal fidelity
|
||||||
findings above (pipe-path `−N` encoding holds; PTY path reduces
|
findings above (pipe-path `−N` encoding holds; PTY path reduces
|
||||||
signal termination to a plain code — portable-pty's exit surface).
|
signal termination to a plain code, portable-pty's exit surface).
|
||||||
|
|
||||||
## OQ seeds carried forward
|
## OQ seeds carried forward
|
||||||
|
|
||||||
1. Session-channel dispatch shape for SOCKS (first-byte sniff vs
|
1. Session-channel dispatch shape for SOCKS (first-byte sniff vs
|
||||||
in-band preamble vs exec marker; the detect window's bound).
|
in-band preamble vs exec marker; the detect window's bound).
|
||||||
2. Exit/signal fidelity over the alktty exit-code convention (the
|
2. Exit/signal fidelity over the alktty exit-code convention (the
|
||||||
`−N` encoding holds on the pipe path — name derivation is
|
`−N` encoding holds on the pipe path, and the name derivation is
|
||||||
server-side; the PTY path's signal-identity loss is an
|
server-side; the PTY path's signal-identity loss is an
|
||||||
alktty/portable-pty matter; pipe-mode pid-only vs PTY
|
alktty/portable-pty matter; pipe-mode pid-only vs PTY
|
||||||
process-group scope).
|
process-group scope).
|
||||||
3. Shell command convention (already seeded; the POC's stand-in —
|
3. Shell command convention (already seeded; the POC's stand-in,
|
||||||
`bash` for shell, `sh -c <cmd>` for exec — is an explicit assembly
|
`bash` for shell and `sh -c <cmd>` for exec, is an explicit assembly
|
||||||
policy, not wire-derived).
|
policy, not wire-derived).
|
||||||
4. The `-R` listen flow (forwarded-tcpip mapping): untouched by the
|
4. The `-R` listen flow (forwarded-tcpip mapping): untouched by the
|
||||||
POC beyond the refusal shape; route choice (ingress via
|
POC beyond the refusal shape; route choice (ingress via
|
||||||
@@ -381,7 +381,7 @@ published sources:
|
|||||||
|
|
||||||
## Reference run
|
## Reference run
|
||||||
|
|
||||||
Full clean run at findings time: **51 checks across 4 stages — 24 + 6
|
Full clean run at findings time: **51 checks across 4 stages: 24 + 6
|
||||||
+ 8 + 13, all PASS, `overall=true`, exit 0** (reproduced twice;
|
+ 8 + 13, all PASS, `overall=true`, exit 0** (reproduced twice;
|
||||||
second run byte-identical PASS set with different ephemeral ports).
|
second run byte-identical PASS set with different ephemeral ports).
|
||||||
Server-side event logs captured per stage in the run output (the
|
Server-side event logs captured per stage in the run output (the
|
||||||
@@ -389,7 +389,7 @@ diagnostics quoted above are verbatim from that run).
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- `pocs.md` §POC-3 (plan), `mapping-table.md` (the rows under test —
|
- `pocs.md` §POC-3 (plan), `mapping-table.md` (the rows under test,
|
||||||
three corrections recorded above), `poc-1-auth-bridge.md` (the auth
|
three corrections recorded above), `poc-1-auth-bridge.md` (the auth
|
||||||
loopback everything rides), `russh.md` (API inventory)
|
loopback everything rides), `russh.md` (API inventory)
|
||||||
- POC source: `/workspace/alkssh-poc3` (`src/stage_{a_git,b_socks,
|
- POC source: `/workspace/alkssh-poc3` (`src/stage_{a_git,b_socks,
|
||||||
|
|||||||
+32
-32
@@ -1,4 +1,4 @@
|
|||||||
# POCs — phase 0 validation plan
|
# POCs: phase 0 validation plan
|
||||||
|
|
||||||
Status: **Complete** (phase 0). All three POCs ran against the current
|
Status: **Complete** (phase 0). All three POCs ran against the current
|
||||||
published russh (0.64.1) and passed; the findings docs
|
published russh (0.64.1) and passed; the findings docs
|
||||||
@@ -6,17 +6,17 @@ published russh (0.64.1) and passed; the findings docs
|
|||||||
`poc-3-mapping-bakeoff.md`) are canonical and supersede the planned
|
`poc-3-mapping-bakeoff.md`) are canonical and supersede the planned
|
||||||
hypothesis wording below wherever they record corrections. POC source
|
hypothesis wording below wherever they record corrections. POC source
|
||||||
is disposable and never merges to main. Mode for all: **standalone**
|
is disposable and never merges to main. Mode for all: **standalone**
|
||||||
(repos have no code to build on yet) — scratch cargo projects at
|
(repos have no code to build on yet), scratch cargo projects at
|
||||||
`/workspace/<poc-name>` unless a POC explicitly builds on repo code
|
`/workspace/<poc-name>` unless a POC explicitly builds on repo code
|
||||||
(then branch mode per `docs/sdd_process.md`).
|
(then branch mode per `docs/sdd_process.md`).
|
||||||
|
|
||||||
The hypotheses below are what phase 1's ADRs will cite. If a POC
|
The hypotheses below are what phase 1's ADRs will cite. If a POC
|
||||||
refutes a hypothesis, the finding doc records the refutation — that is
|
refutes a hypothesis, the finding doc records the refutation, which is
|
||||||
the point of running it.
|
the point of running it.
|
||||||
|
|
||||||
## POC-1 — auth bridge + fail-closed declines end to end
|
## POC-1: auth bridge + fail-closed declines end to end
|
||||||
|
|
||||||
Findings: `poc-1-auth-bridge.md` (complete — 31/31; all hypotheses
|
Findings: `poc-1-auth-bridge.md` (complete: 31/31; all hypotheses
|
||||||
confirmed, one correction to the research docs' russh default-impl
|
confirmed, one correction to the research docs' russh default-impl
|
||||||
assumption). POC source: `/workspace/alkssh-poc1`.
|
assumption). POC source: `/workspace/alkssh-poc1`.
|
||||||
|
|
||||||
@@ -42,9 +42,9 @@ keys; russh client available as a secondary client for
|
|||||||
with OpenSSH's probe-then-sign flow.
|
with OpenSSH's probe-then-sign flow.
|
||||||
- Decline matrix: `auth_none`/`password`/`keyboard-interactive`
|
- Decline matrix: `auth_none`/`password`/`keyboard-interactive`
|
||||||
declined (0.64.1 correction: the *default impls already reject* at
|
declined (0.64.1 correction: the *default impls already reject* at
|
||||||
this russh — only `auth_publickey_offered` defaults to accept;
|
this russh: only `auth_publickey_offered` defaults to accept;
|
||||||
`poc-1-auth-bridge.md` §3. The family contract still requires
|
`poc-1-auth-bridge.md` §3). The family contract still requires
|
||||||
implementing every hook explicitly — the pin is a one-way door);
|
implementing every hook explicitly, since the pin is a one-way door;
|
||||||
`Config::methods` advertises `publickey` only.
|
`Config::methods` advertises `publickey` only.
|
||||||
- `ChannelOpenHandle` drop = auto-reject semantics; unfeatured
|
- `ChannelOpenHandle` drop = auto-reject semantics; unfeatured
|
||||||
channel-type declines observable by vanilla `ssh`
|
channel-type declines observable by vanilla `ssh`
|
||||||
@@ -56,12 +56,12 @@ keys; russh client available as a secondary client for
|
|||||||
uncertainty is fingerprint-format parity details, which this either
|
uncertainty is fingerprint-format parity details, which this either
|
||||||
confirms or corrects.
|
confirms or corrects.
|
||||||
|
|
||||||
## POC-2 — client-side at-rest key handling (vault-backed storage)
|
## POC-2: client-side at-rest key handling (vault-backed storage)
|
||||||
|
|
||||||
Findings: `poc-2-vault-keys.md` (complete — 39/39; OpenSSH-PEM-inside-
|
Findings: `poc-2-vault-keys.md` (complete: 39/39; OpenSSH-PEM-inside-
|
||||||
`EncryptedData` confirmed, Signer seam dismissed with evidence).
|
`EncryptedData` confirmed, Signer seam dismissed with evidence).
|
||||||
|
|
||||||
**Question:** how are held SSH keys stored and used — the vault as
|
**Question:** how are held SSH keys stored and used: the vault as
|
||||||
pure at-rest encryption (the key is just another bearer-token-class
|
pure at-rest encryption (the key is just another bearer-token-class
|
||||||
secret: opaque bytes, vault-encrypted at rest, decrypted at use), what
|
secret: opaque bytes, vault-encrypted at rest, decrypted at use), what
|
||||||
byte format sits inside, and how does the decrypt→dial lifecycle
|
byte format sits inside, and how does the decrypt→dial lifecycle
|
||||||
@@ -82,39 +82,39 @@ the key's fingerprint through the same stub `IdentityProvider`).
|
|||||||
→ zeroized handling for the dial's lifetime; what happens locked /
|
→ zeroized handling for the dial's lifetime; what happens locked /
|
||||||
absent (fail-closed no-dial) / mid-session unlock rotation.
|
absent (fail-closed no-dial) / mid-session unlock rotation.
|
||||||
- Whether a `Signer`-shaped seam is still worth anything given the
|
- Whether a `Signer`-shaped seam is still worth anything given the
|
||||||
bytes are handled client-side at use anyway (expected answer: no —
|
bytes are handled client-side at use anyway (expected answer: no,
|
||||||
recorded so the ADR can dismiss it with evidence).
|
recorded so the ADR can dismiss it with evidence).
|
||||||
- Server and client host-key material riding the same model.
|
- Server and client host-key material riding the same model.
|
||||||
|
|
||||||
**Scope note:** the vault is for secrets *we* hold — client-side
|
**Scope note:** the vault is for secrets *we* hold (client-side
|
||||||
outgoing keys and the server's own host key — and is explicitly not
|
outgoing keys and the server's own host key) and is explicitly not
|
||||||
involved in resolving *clients'* fingerprints (that path is the
|
involved in resolving *clients'* fingerprints (that path is the
|
||||||
assembly-layer `IdentityProvider`). The vault's derivation surface
|
assembly-layer `IdentityProvider`). The vault's derivation surface
|
||||||
(`derive_ed25519`, SLIP-0010 paths) is not on this path either — that
|
(`derive_ed25519`, SLIP-0010 paths) is not on this path either; that
|
||||||
is a different alkvault feature, not SSH key custody. Kept as its own
|
is a different alkvault feature, not SSH key custody. Kept as its own
|
||||||
POC because its failure modes (at-rest/lifecycle, zeroization, lock
|
POC because its failure modes (at-rest/lifecycle, zeroization, lock
|
||||||
behavior) are unrelated to the auth-flow failure modes of POC-1.
|
behavior) are unrelated to the auth-flow failure modes of POC-1.
|
||||||
|
|
||||||
**Hypothesized outcome:** OpenSSH-encoded key inside alkvault
|
**Hypothesized outcome:** OpenSSH-encoded key inside alkvault
|
||||||
`EncryptedData`, one-time `decode_secret_key` at dial, zeroizing
|
`EncryptedData`, one-time `decode_secret_key` at dial, zeroizing
|
||||||
wrapper — confirming the alkhttp ADR-014 analogy end to end.
|
wrapper, confirming the alkhttp ADR-014 analogy end to end.
|
||||||
|
|
||||||
## POC-3 — the mapping bake-off: channel bridge through a real sibling seam
|
## POC-3: the mapping bake-off, channel bridge through a real sibling seam
|
||||||
|
|
||||||
Findings: `poc-3-mapping-bakeoff.md` (complete — 51/51 across stages
|
Findings: `poc-3-mapping-bakeoff.md` (complete: 51/51 across stages
|
||||||
A–D; every hypothesis confirmed, three mapping-table corrections
|
A–D; every hypothesis confirmed, three mapping-table corrections
|
||||||
recorded, plus the russh inline-dispatch rule). POC source:
|
recorded, plus the russh inline-dispatch rule). POC source:
|
||||||
`/workspace/alkssh-poc3`.
|
`/workspace/alkssh-poc3`.
|
||||||
|
|
||||||
**Question:** does "russh channel duplex → family serve-over-a-stream
|
**Question:** does "russh channel duplex → family serve-over-a-stream
|
||||||
entry" actually work against the real published seams — and which of
|
entry" actually work against the real published seams, and which of
|
||||||
the mapping-table's route choices survives contact with reality?
|
the mapping-table's route choices survives contact with reality?
|
||||||
|
|
||||||
**Construction:** standalone, but *depending on the real sibling
|
**Construction:** standalone, but *depending on the real sibling
|
||||||
crates* (alktty/alksocks/alkgit + alkcall types via their published
|
crates* (alktty/alksocks/alkgit + alkcall types via their published
|
||||||
forms). russh server loopback + vanilla clients; per stage:
|
forms). russh server loopback + vanilla clients; per stage:
|
||||||
|
|
||||||
- **Stage A — git exec mapping** (the alkforge-critical path): vanilla
|
- **Stage A, git exec mapping** (the alkforge-critical path): vanilla
|
||||||
`git clone/push` over `ssh://` → russh server → parse exec command →
|
`git clone/push` over `ssh://` → russh server → parse exec command →
|
||||||
`serve_duplex_resolved(...)` over the split channel halves with stub
|
`serve_duplex_resolved(...)` over the split channel halves with stub
|
||||||
`GitRefs`/`GitPackGen`/`GitPackIngest` (or the substrate composition
|
`GitRefs`/`GitPackGen`/`GitPackIngest` (or the substrate composition
|
||||||
@@ -122,26 +122,26 @@ forms). russh server loopback + vanilla clients; per stage:
|
|||||||
(server-speaks-first advertisement) dialect satisfies vanilla git's
|
(server-speaks-first advertisement) dialect satisfies vanilla git's
|
||||||
expectations over exec; channel EOF/close maps to session
|
expectations over exec; channel EOF/close maps to session
|
||||||
termination; exit-status flows.
|
termination; exit-status flows.
|
||||||
- **Stage B — `-D` mapping**: russh server, feature `dynamic-forward`-
|
- **Stage B, `-D` mapping**: russh server, feature `dynamic-forward`-
|
||||||
shaped build → `serve_socks5_over` over a session channel → `curl`
|
shaped build → `serve_socks5_over` over a session channel → `curl`
|
||||||
through the SOCKS5 proxy to a local HTTP endpoint. Confirms: the
|
through the SOCKS5 proxy to a local HTTP endpoint. Confirms: the
|
||||||
dispatch-split inside `session` (parse-then-route vs SOCKS-detect),
|
dispatch-split inside `session` (parse-then-route vs SOCKS-detect),
|
||||||
duplex backpressure under throughput.
|
duplex backpressure under throughput.
|
||||||
- **Stage C — `-L` mapping**: `direct-tcpip` → the mapping-table's two
|
- **Stage C, `-L` mapping**: `direct-tcpip` → the mapping-table's two
|
||||||
alktunnels routes (adapter-side `TunnelSession::open_direct` via a
|
alktunnels routes (adapter-side `TunnelSession::open_direct` via a
|
||||||
real `ChannelClient` vs local-dial stand-in). Whichever route the
|
real `ChannelClient` vs local-dial stand-in). Whichever route the
|
||||||
POC exercises informs the phase 1 ADR on the presence/absence of a
|
POC exercises informs the phase 1 ADR on the presence/absence of a
|
||||||
serve-over-stream entry in alktunnels.
|
serve-over-stream entry in alktunnels.
|
||||||
- **Stage D — session/pty mapping**, final stage (ran in full): shell,
|
- **Stage D, session/pty mapping**, final stage (ran in full): shell,
|
||||||
exec, window-change and signal mapping against a real PTY
|
exec, window-change and signal mapping against a real PTY
|
||||||
(`LocalTtyBackend`, alktty `local`). The "optional" hedge in the
|
(`LocalTtyBackend`, alktty `local`). The "optional" hedge in the
|
||||||
original plan was unnecessary — stage D completed 13/13 and produced
|
original plan was unnecessary: stage D completed 13/13 and produced
|
||||||
the load-bearing session-row corrections: the alktty bridge is a
|
the load-bearing session-row corrections: the alktty bridge is a
|
||||||
**translation bridge** (chunk framing both directions, not a byte
|
**translation bridge** (chunk framing both directions, not a byte
|
||||||
pump) and the exit/signal findings over the alktty exit-code
|
pump) and the exit/signal findings over the alktty exit-code
|
||||||
convention (pipe-path signal termination rides the negative code
|
convention (pipe-path signal termination rides the negative code
|
||||||
`−N`; the PTY path reduces signal termination to a plain exit
|
`−N`; the PTY path reduces signal termination to a plain exit
|
||||||
code — `poc-3-mapping-bakeoff.md` §D.2).
|
code, per `poc-3-mapping-bakeoff.md` §D.2).
|
||||||
|
|
||||||
**Validates:** the one-seam-per-mapping hypothesis; the parse-then-
|
**Validates:** the one-seam-per-mapping hypothesis; the parse-then-
|
||||||
route dispatch; stderr semantics in pipe vs PTY mode; EOF/close/signal
|
route dispatch; stderr semantics in pipe vs PTY mode; EOF/close/signal
|
||||||
@@ -160,14 +160,14 @@ vanilla-SSH server and consumes its surface (exec/pty channels,
|
|||||||
`direct-tcpip` opens, the `tcpip_forward` + accepted-`forwarded-tcpip`
|
`direct-tcpip` opens, the `tcpip_forward` + accepted-`forwarded-tcpip`
|
||||||
listener flow). No dedicated POC was needed: the POCs exercised the
|
listener flow). No dedicated POC was needed: the POCs exercised the
|
||||||
client API throughout (POC-1's russh-client auth leg, POC-3 §D.3's
|
client API throughout (POC-1's russh-client auth leg, POC-3 §D.3's
|
||||||
client-side request mechanics — `Channel::exec()`, the `wait()`-drain
|
client-side request mechanics (`Channel::exec()`, the `wait()`-drain
|
||||||
trap, fire-and-forget `window_change`/`signal` before `into_stream()`).
|
trap, fire-and-forget `window_change`/`signal` before `into_stream()`)).
|
||||||
|
|
||||||
Two client-side facts phase 1 designs from:
|
Two client-side facts phase 1 designs from:
|
||||||
|
|
||||||
1. **The acceptor rule.** The only server-initiated channel type in the
|
1. **The acceptor rule.** The only server-initiated channel type in the
|
||||||
pattern is `forwarded-tcpip`, and russh's client default for an
|
pattern is `forwarded-tcpip`, and russh's client default for an
|
||||||
unhandled forwarded open is *accept-and-drop* — which is not the
|
unhandled forwarded open is *accept-and-drop*, which is not the
|
||||||
family fail-closed posture. The client-side accepted-open
|
family fail-closed posture. The client-side accepted-open
|
||||||
disposition (accept into a registered handler vs decline) is
|
disposition (accept into a registered handler vs decline) is
|
||||||
alkssh's contract to define.
|
alkssh's contract to define.
|
||||||
@@ -179,9 +179,9 @@ Two client-side facts phase 1 designs from:
|
|||||||
|
|
||||||
## Sequencing and gates
|
## Sequencing and gates
|
||||||
|
|
||||||
1. POC-1 first — everything downstream presumes its auth plumbing.
|
1. POC-1 first; everything downstream presumes its auth plumbing.
|
||||||
2. POC-2 after POC-1 (reuses POC-1's server loopback).
|
2. POC-2 after POC-1 (reuses POC-1's server loopback).
|
||||||
3. POC-3 last; stage A gates phase 1's mapping ADR work — it is the
|
3. POC-3 last; stage A gates phase 1's mapping ADR work; it is the
|
||||||
path alkforge phase 0 consumes first.
|
path alkforge phase 0 consumes first.
|
||||||
4. Phase 0 completed when the findings docs existed and the OQ list in
|
4. Phase 0 completed when the findings docs existed and the OQ list in
|
||||||
`vision.md` §scope questions was either answered by findings or
|
`vision.md` §scope questions was either answered by findings or
|
||||||
@@ -191,12 +191,12 @@ Two client-side facts phase 1 designs from:
|
|||||||
verdicts; scope question 4 (auth dispositions + client-side vault
|
verdicts; scope question 4 (auth dispositions + client-side vault
|
||||||
keys) is answered per POC-1/POC-2; scope question 2 (russh pin
|
keys) is answered per POC-1/POC-2; scope question 2 (russh pin
|
||||||
facts) is verified; questions 1 (the `alk/ssh` topology) and 3
|
facts) is verified; questions 1 (the `alk/ssh` topology) and 3
|
||||||
(feature naming) are decision territory carried as phase 1 OQs —
|
(feature naming) are decision territory carried as phase 1 OQs, and
|
||||||
the seeded OQ list is in each findings doc.
|
the seeded OQ list is in each findings doc.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- `vision.md` (principles the POCs must honor — including that POC
|
- `vision.md` (principles the POCs must honor, including that POC
|
||||||
code carries relaxed constraints but never merges)
|
code carries relaxed constraints but never merges)
|
||||||
- `russh.md` (API shapes the POCs exercise), `auth-bridge.md`,
|
- `russh.md` (API shapes the POCs exercise), `auth-bridge.md`,
|
||||||
`mapping-table.md`
|
`mapping-table.md`
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Reverse runner — client-direction pattern extraction (alkminer)
|
# Reverse runner: client-direction pattern extraction (alkminer)
|
||||||
|
|
||||||
Status: phase 0 post-POC extraction. **Reading aid only — concepts and
|
Status: phase 0 post-POC extraction. **Reading aid only: concepts and
|
||||||
wire shapes, never code** (the source is not open-sourced and stays
|
wire shapes, never code** (the source is not open-sourced and stays
|
||||||
that way; per repo license hygiene, reference code is for reading and
|
that way; per repo license hygiene, reference code is for reading and
|
||||||
facts). Source:
|
facts). Source:
|
||||||
`/workspace/@alkminer/alkminer` — specifically
|
`/workspace/@alkminer/alkminer`, specifically
|
||||||
`crates/alkminer-host/src/remote/{ssh,exec,forward,manager,sftp}.rs`
|
`crates/alkminer-host/src/remote/{ssh,exec,forward,manager,sftp}.rs`
|
||||||
and `src/oai/mod.rs`. Also old (russh 0.62 era) and POC-grade; its
|
and `src/oai/mod.rs`. Also old (russh 0.62 era) and POC-grade; its
|
||||||
weak spots are as instructive as its strengths, and both are recorded
|
weak spots are as instructive as its strengths, and both are recorded
|
||||||
@@ -14,19 +14,19 @@ below as facts to design from, not precedents to copy.
|
|||||||
|
|
||||||
The reverse-runner pattern inverts the typical runner flow: the
|
The reverse-runner pattern inverts the typical runner flow: the
|
||||||
**hub/coordinator is the SSH client** and dials out to vanilla `sshd`
|
**hub/coordinator is the SSH client** and dials out to vanilla `sshd`
|
||||||
on rented containers (vast.ai, runpod — any box that exposes SSH). The
|
on rented containers (vast.ai, runpod, any box that exposes SSH). The
|
||||||
runner exposes nothing but the wire; the hub is the active party and
|
runner exposes nothing but the wire; the hub is the active party and
|
||||||
pushes all work and all channel opens.
|
pushes all work and all channel opens.
|
||||||
|
|
||||||
Security shape (why this beat the alternatives in practice):
|
Security shape (why this beat the alternatives in practice):
|
||||||
|
|
||||||
- **No inbound surface on runners** — only `sshd` with one key.
|
- **No inbound surface on runners**, only `sshd` with one key.
|
||||||
- **Secrets never leave the hub** — the pattern's killer use was an
|
- **Secrets never leave the hub**; the pattern's killer use was an
|
||||||
OAI-compatible proxy served *by the hub* (`oai/mod.rs`, alkminer
|
OAI-compatible proxy served *by the hub* (`oai/mod.rs`, alkminer
|
||||||
ADR-016): the instance points its LLM tooling at a localhost URL
|
ADR-016): the instance points its LLM tooling at a localhost URL
|
||||||
that rides the tunnel to the hub's proxy, which injects the real
|
that rides the tunnel to the hub's proxy, which injects the real
|
||||||
provider key. The runner authenticates to the proxy with a scoped
|
provider key. The runner authenticates to the proxy with a scoped
|
||||||
dev token — least privilege, no upstream-secret custody.
|
dev token: least privilege, no upstream-secret custody.
|
||||||
- **Reachback both ways, deliberately**: hub→runner services via `-L`,
|
- **Reachback both ways, deliberately**: hub→runner services via `-L`,
|
||||||
runner→hub services via `-R` (the two legs below were both used,
|
runner→hub services via `-R` (the two legs below were both used,
|
||||||
sometimes chained: instance port ‑L→ hub port ‑L→ dev workstation).
|
sometimes chained: instance port ‑L→ hub port ‑L→ dev workstation).
|
||||||
@@ -39,9 +39,9 @@ One authenticated SSH connection per instance carried:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| blocking `exec` | provisioning commands, health checks | session channel open + `exec()`; alktty pipe-mode seam when the runner is family-built |
|
| blocking `exec` | provisioning commands, health checks | session channel open + `exec()`; alktty pipe-mode seam when the runner is family-built |
|
||||||
| background jobs (`start_job`/`job_status`/`job_logs`/`stop_job`) | mining worker lifecycle; pid files on the runner, job rows in hub-side storage | assembly-level job supervision over exec channels; the alkminer pid-file approach is the ad-hoc analog of POC-3 §D's pipe-mode signal gap (alktty exit-chunk signal identity OQ) |
|
| background jobs (`start_job`/`job_status`/`job_logs`/`stop_job`) | mining worker lifecycle; pid files on the runner, job rows in hub-side storage | assembly-level job supervision over exec channels; the alkminer pid-file approach is the ad-hoc analog of POC-3 §D's pipe-mode signal gap (alktty exit-chunk signal identity OQ) |
|
||||||
| SFTP | binary deploy to the instance | **future alkfs client half** — real consumer evidence that the `sftp` feature's deferred implementation needs both directions (alkforge CI artifact deploy is the consumer) |
|
| SFTP | binary deploy to the instance | **future alkfs client half**: real consumer evidence that the `sftp` feature's deferred implementation needs both directions (alkforge CI artifact deploy is the consumer) |
|
||||||
| `-L` client-side tunnel | opencode dev tunnel (instance:4096 → hub) | client opens `direct-tcpip` per accepted local connection and pumps (POC-1's `into_stream` plumbing, reversed) |
|
| `-L` client-side tunnel | opencode dev tunnel (instance:4096 → hub) | client opens `direct-tcpip` per accepted local connection and pumps (POC-1's `into_stream` plumbing, reversed) |
|
||||||
| `-R` client-side tunnel | hub-service reach tunnel (instance → hub's HTTP/OAI proxy) | `tcpip_forward` global request (returns the bound port — no mpsc sink param, matching `russh.md`'s client inventory) then **accept the server-initiated `forwarded-tcpip` opens** and relay each to the hub-local target |
|
| `-R` client-side tunnel | hub-service reach tunnel (instance → hub's HTTP/OAI proxy) | `tcpip_forward` global request (returns the bound port; no mpsc sink param, matching `russh.md`'s client inventory) then **accept the server-initiated `forwarded-tcpip` opens** and relay each to the hub-local target |
|
||||||
|
|
||||||
## Client-direction facts that phase 1 designs from
|
## Client-direction facts that phase 1 designs from
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ One authenticated SSH connection per instance carried:
|
|||||||
client handler receives `server_channel_open_forwarded_tcpip` for
|
client handler receives `server_channel_open_forwarded_tcpip` for
|
||||||
`-R` data. In alkminer's 0.62-era code (and unchanged in kind at
|
`-R` data. In alkminer's 0.62-era code (and unchanged in kind at
|
||||||
0.64.1), an accepted forwarded channel with no handler installed is
|
0.64.1), an accepted forwarded channel with no handler installed is
|
||||||
**accept-and-drop** — a fail-open-ish default. alkssh's client
|
**accept-and-drop**, a fail-open-ish default. alkssh's client
|
||||||
contract: only channel types/features the assembly registered are
|
contract: only channel types/features the assembly registered are
|
||||||
served; otherwise decline cleanly. This is the client side of the
|
served; otherwise decline cleanly. This is the client side of the
|
||||||
feature-graph symmetry rule; unlike the server auth defaults (which
|
feature-graph symmetry rule; unlike the server auth defaults (which
|
||||||
@@ -60,11 +60,11 @@ One authenticated SSH connection per instance carried:
|
|||||||
(`install_forward_sink`) set after `connect` by the consumer that
|
(`install_forward_sink`) set after `connect` by the consumer that
|
||||||
owns the forward. The family translation is an assembly-injected
|
owns the forward. The family translation is an assembly-injected
|
||||||
acceptor-handlers registry (per channel type / per feature), not a
|
acceptor-handlers registry (per channel type / per feature), not a
|
||||||
static config — but the *late-install* dynamic is real: consumers
|
static config, but the *late-install* dynamic is real: consumers
|
||||||
turn capabilities on/off over a live connection.
|
turn capabilities on/off over a live connection.
|
||||||
3. **Keepalives are load-bearing on cloud runners.** The hub needed
|
3. **Keepalives are load-bearing on cloud runners.** The hub needed
|
||||||
`keepalive_interval` 15 s + `keepalive_max` 3 (OpenSSH
|
`keepalive_interval` 15 s + `keepalive_max` 3 (OpenSSH
|
||||||
`ServerAliveInterval=15`/`ServerAliveCountMax=3` parity) — with
|
`ServerAliveInterval=15`/`ServerAliveCountMax=3` parity). With
|
||||||
russh's default (no keepalives), dead NAT'd connections sat
|
russh's default (no keepalives), dead NAT'd connections sat
|
||||||
unnoticed until a health-check exec failed. Evidence for the
|
unnoticed until a health-check exec failed. Evidence for the
|
||||||
client-side bounded-resources defaults (the server side has
|
client-side bounded-resources defaults (the server side has
|
||||||
@@ -72,28 +72,28 @@ One authenticated SSH connection per instance carried:
|
|||||||
4. **Reconnect-and-restore semantics.** Tunnel ports were persisted
|
4. **Reconnect-and-restore semantics.** Tunnel ports were persisted
|
||||||
(hub storage) and re-bound identically on reconnect; forwards were
|
(hub storage) and re-bound identically on reconnect; forwards were
|
||||||
tracked per connection with idempotent stop handles. This is
|
tracked per connection with idempotent stop handles. This is
|
||||||
assembly policy (session/forward lifecycle), not wire contract —
|
assembly policy (session/forward lifecycle), not wire contract;
|
||||||
kept in view so phase 1 doesn't accidentally bake reconnect policy
|
kept in view so phase 1 doesn't accidentally bake reconnect policy
|
||||||
into the crate.
|
into the crate.
|
||||||
5. **Host-key acceptance for ephemeral fleets — the pattern the fleet
|
5. **Host-key acceptance for ephemeral fleets: the pattern the fleet
|
||||||
model demands.** alkminer's `check_server_key` returned accept-all
|
model demands.** alkminer's `check_server_key` returned accept-all
|
||||||
for rental fleets. That was the correct pattern for that problem,
|
for rental fleets. That was the correct pattern for that problem,
|
||||||
not a lapse: a fresh container per rent mints a fresh host key, so
|
not a lapse: a fresh container per rent mints a fresh host key, so
|
||||||
no pin can exist beforehand — strict TOFU/"pin everything" is
|
no pin can exist beforehand, so strict TOFU/"pin everything" is
|
||||||
unusable there, and the churn is exactly why accept-all won. The
|
unusable there, and the churn is exactly why accept-all won. The
|
||||||
security anchor is elsewhere: hub-held per-instance dial keys make
|
security anchor is elsewhere: hub-held per-instance dial keys make
|
||||||
a captured hub credential worthless, plus what rides the tunnels.
|
a captured hub credential worthless, plus what rides the tunnels.
|
||||||
The family rule is therefore not "pin everything" — it is "no
|
The family rule is therefore not "pin everything"; it is "no
|
||||||
trust decisions inside the crate": default fail-closed at the
|
trust decisions inside the crate": default fail-closed at the
|
||||||
crate, with an assembly-provided policy seam so each fleet model
|
crate, with an assembly-provided policy seam so each fleet model
|
||||||
gets the control that fits it — host pinning where anchors can
|
gets the control that fits it: host pinning where anchors can
|
||||||
exist (family-built runners, whose host key the assembly mints and
|
exist (family-built runners, whose host key the assembly mints and
|
||||||
therefore knows beforehand), permissive accept where they can't
|
therefore knows beforehand), permissive accept where they can't
|
||||||
(rental fleets). The fleet churn is the reason the two-case policy
|
(rental fleets). The fleet churn is the reason the two-case policy
|
||||||
seam (OQ-05) is the correct shape rather than "pin everything".
|
seam (OQ-05) is the correct shape rather than "pin everything".
|
||||||
6. **Per-runner key registration.** Keys were minted per instance and
|
6. **Per-runner key registration.** Keys were minted per instance and
|
||||||
registered hub-side (their provisioning flow, including a real
|
registered hub-side (their provisioning flow, including a real
|
||||||
gitea client for dev workspaces) — the deployment precedent for
|
gitea client for dev workspaces), is the deployment precedent for
|
||||||
alkforge's per-runner `IdentityProvider` rows.
|
alkforge's per-runner `IdentityProvider` rows.
|
||||||
|
|
||||||
## Not carried
|
## Not carried
|
||||||
@@ -103,7 +103,7 @@ One authenticated SSH connection per instance carried:
|
|||||||
files on the runner) belongs to the assembly layer in a family
|
files on the runner) belongs to the assembly layer in a family
|
||||||
deployment; the crate's surface is channels and tunnels.
|
deployment; the crate's surface is channels and tunnels.
|
||||||
- No per-target scoping of proxied traffic existed (dev token proved
|
- No per-target scoping of proxied traffic existed (dev token proved
|
||||||
"a valid dev instance", nothing more) — a real least-privilege gap
|
"a valid dev instance", nothing more), a real least-privilege gap
|
||||||
the family model fixes via per-identity ACLs on the service side.
|
the family model fixes via per-identity ACLs on the service side.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
@@ -115,4 +115,4 @@ One authenticated SSH connection per instance carried:
|
|||||||
`poc-3-mapping-bakeoff.md` §D.3 (client-request mechanics)
|
`poc-3-mapping-bakeoff.md` §D.3 (client-request mechanics)
|
||||||
- `auth-bridge.md` §Host keys and `check_server_key` (the trust-policy
|
- `auth-bridge.md` §Host keys and `check_server_key` (the trust-policy
|
||||||
seam this doc's ephemeral-fleet acceptance pattern motivates)
|
seam this doc's ephemeral-fleet acceptance pattern motivates)
|
||||||
- alkminer ADR-016 (the OAI proxy pattern — secrets stay at the hub)
|
- alkminer ADR-016 (the OAI proxy pattern: secrets stay at the hub)
|
||||||
+35
-35
@@ -1,4 +1,4 @@
|
|||||||
# russh — crate inventory and pin considerations
|
# russh: crate inventory and pin considerations
|
||||||
|
|
||||||
Status: Draft (phase 0 research). Facts from docs.rs and the GitHub
|
Status: Draft (phase 0 research). Facts from docs.rs and the GitHub
|
||||||
release notes as of 2026-10-25; versions re-verified at architecture
|
release notes as of 2026-10-25; versions re-verified at architecture
|
||||||
@@ -6,7 +6,7 @@ time before any pin is recorded.
|
|||||||
|
|
||||||
## Version landscape
|
## Version landscape
|
||||||
|
|
||||||
**Re-verified at architecture time (2026-10-26) — see the section at
|
**Re-verified at architecture time (2026-10-26); see the section at
|
||||||
the end of this file.** russh **0.65.0** was published 2026-10-25
|
the end of this file.** russh **0.65.0** was published 2026-10-25
|
||||||
(after the POCs verified 0.64.1 as newest); the pin ADR is written
|
(after the POCs verified 0.64.1 as newest); the pin ADR is written
|
||||||
against 0.65.0. The POC wire-behavior evidence transfers: the 0.65.0
|
against 0.65.0. The POC wire-behavior evidence transfers: the 0.65.0
|
||||||
@@ -25,22 +25,22 @@ breaking changes are handler-signature shapes, not wire behavior.
|
|||||||
GHSA-3hm4-p7h4-rvwp (pre-auth stack exhaustion via nested OpenSSH
|
GHSA-3hm4-p7h4-rvwp (pre-auth stack exhaustion via nested OpenSSH
|
||||||
certificates); the agent/pageant fixes are off our path (agent
|
certificates); the agent/pageant fixes are off our path (agent
|
||||||
forwarding declines, ADR-003).
|
forwarding declines, ADR-003).
|
||||||
- 0.64.1 (2026-10-05) was the POC pin — security-fix-only patch of
|
- 0.64.1 (2026-10-05) was the POC pin, a security-fix-only patch of
|
||||||
0.64.0 (GHSA-4wc5-f2rc-q74m — unbounded memory allocation in the
|
0.64.0 (GHSA-4wc5-f2rc-q74m, unbounded memory allocation in the
|
||||||
agent protocol server; no API change). The POC findings' evidence
|
agent protocol server; no API change). The POC findings' evidence
|
||||||
and this file's earlier inventory facts were verified against
|
and this file's earlier inventory facts were verified against
|
||||||
0.64.1 and carry to 0.65.0 as noted above.
|
0.64.1 and carry to 0.65.0 as noted above.
|
||||||
- 0.64.0 (2026-10-04) broke API: dropped the
|
- 0.64.0 (2026-10-04) broke API: dropped the
|
||||||
`legacy-ed25519-pkcs8-parser` feature (pre-0.43 non-compliant
|
`legacy-ed25519-pkcs8-parser` feature (pre-0.43 non-compliant
|
||||||
ed25519 keys now hard-error); fixed GHSA-j7h2-66j4-ghjf (server
|
ed25519 keys now hard-error); fixed GHSA-j7h2-66j4-ghjf (server
|
||||||
pubkey auth accepted mismatched signature algorithms — e.g.
|
pubkey auth accepted mismatched signature algorithms, e.g.
|
||||||
rsa-sha2-512 silently downgraded to rsa-sha1); added
|
rsa-sha2-512 silently downgraded to rsa-sha1); added
|
||||||
`Handle::hostkeys_prove` and `Handle::send_global_request`; fixed
|
`Handle::hostkeys_prove` and `Handle::send_global_request`; fixed
|
||||||
`Limits` not being fully enforced.
|
`Limits` not being fully enforced.
|
||||||
- There are older russh families (0.4x–0.5x) with materially different
|
- There are older russh families (0.4x–0.5x) with materially different
|
||||||
handler shapes in circulation; one rough family-internal integration
|
handler shapes in circulation; one rough family-internal integration
|
||||||
example exists (alknet `main`, pinned 0.49) but it is a reading aid
|
example exists (alknet `main`, pinned 0.49) but it is a reading aid
|
||||||
only — a rough working example, months stale — and is **not decision
|
only: a rough working example, months stale, and **not decision
|
||||||
input** for anything here. The pin is chosen against the current
|
input** for anything here. The pin is chosen against the current
|
||||||
published russh, verified at architecture time.
|
published russh, verified at architecture time.
|
||||||
|
|
||||||
@@ -52,14 +52,14 @@ breaking changes are handler-signature shapes, not wire behavior.
|
|||||||
our license satisfies it); record this in the pin ADR so the audit
|
our license satisfies it); record this in the pin ADR so the audit
|
||||||
trail is explicit.
|
trail is explicit.
|
||||||
- **MSRV 1.89, edition 2024.** Our crate's `rust-version` must be ≥
|
- **MSRV 1.89, edition 2024.** Our crate's `rust-version` must be ≥
|
||||||
russh's once the dependency lands (currently `1.88` in the manifest
|
russh's once the dependency lands (currently `1.88` in the manifest,
|
||||||
— a pending decision, deferred to the pin ADR).
|
a pending decision, deferred to the pin ADR).
|
||||||
- **Crypto backend: exactly one of `aws-lc-rs` (default) or `ring`.**
|
- **Crypto backend: exactly one of `aws-lc-rs` (default) or `ring`.**
|
||||||
The build errors if zero or both are selected. Feature choice is a
|
The build errors if zero or both are selected. Feature choice is a
|
||||||
phase 1 decision (alktls family precedent may pull one way).
|
phase 1 decision (alktls family precedent may pull one way).
|
||||||
- Other features: `flate2` (compression, default), `rsa` (default;
|
- Other features: `flate2` (compression, default), `rsa` (default;
|
||||||
pulls **RC-pinned** `rsa =0.10.0-rc.18` + `pkcs1 =0.8.0-rc.4`),
|
pulls **RC-pinned** `rsa =0.10.0-rc.18` + `pkcs1 =0.8.0-rc.4`),
|
||||||
`async-trait`, `des`/`dsa` ("Danger: insecure" — never enable),
|
`async-trait`, `des`/`dsa` ("Danger: insecure"; never enable),
|
||||||
`serde`. The `rsa` question interacts with the RC-pin question below.
|
`serde`. The `rsa` question interacts with the RC-pin question below.
|
||||||
- **RC dependencies in the current pin set:** `ssh-key =0.7.0-rc.11`
|
- **RC dependencies in the current pin set:** `ssh-key =0.7.0-rc.11`
|
||||||
(exact-pinned by russh itself via `russh::keys::ssh_key` re-export),
|
(exact-pinned by russh itself via `russh::keys::ssh_key` re-export),
|
||||||
@@ -77,7 +77,7 @@ Required: `type Error: From<russh::Error> + Send`. Everything else is
|
|||||||
provided methods; at 0.64.1 the default auth impls are **reject**
|
provided methods; at 0.64.1 the default auth impls are **reject**
|
||||||
(`Auth::reject()`) for `auth_none`, `auth_password`,
|
(`Auth::reject()`) for `auth_none`, `auth_password`,
|
||||||
`auth_keyboard_interactive`, `auth_publickey`, and
|
`auth_keyboard_interactive`, `auth_publickey`, and
|
||||||
`auth_openssh_certificate` — only `auth_publickey_offered` defaults to
|
`auth_openssh_certificate`; only `auth_publickey_offered` defaults to
|
||||||
accept (verified in 0.64.1 source; POC-1 corrected this doc's earlier
|
accept (verified in 0.64.1 source; POC-1 corrected this doc's earlier
|
||||||
accept-defaults assumption). Non-auth defaults decline/no-op. The
|
accept-defaults assumption). Non-auth defaults decline/no-op. The
|
||||||
fail-closed discipline is nonetheless *ours to implement explicitly*
|
fail-closed discipline is nonetheless *ours to implement explicitly*
|
||||||
@@ -86,7 +86,7 @@ on a bump), and POC-1 proves the explicit implementations. Key
|
|||||||
inventory:
|
inventory:
|
||||||
|
|
||||||
- Auth: `auth_none(user)`, `auth_password(user, password)`,
|
- Auth: `auth_none(user)`, `auth_password(user, password)`,
|
||||||
`auth_publickey_offered(user, &PublicKey)` (pre-auth probe — cheap
|
`auth_publickey_offered(user, &PublicKey)` (pre-auth probe, a cheap
|
||||||
rejection hook; key ownership not yet verified),
|
rejection hook; key ownership not yet verified),
|
||||||
`auth_publickey(user, &PublicKey)` (called **after** russh verified
|
`auth_publickey(user, &PublicKey)` (called **after** russh verified
|
||||||
the signature and key ownership), `auth_openssh_certificate(user,
|
the signature and key ownership), `auth_openssh_certificate(user,
|
||||||
@@ -96,7 +96,7 @@ inventory:
|
|||||||
`auth_succeeded(&mut Session)` fires on success.
|
`auth_succeeded(&mut Session)` fires on success.
|
||||||
- Channel opens (each receives `channel: Channel<Msg>` +
|
- Channel opens (each receives `channel: Channel<Msg>` +
|
||||||
`reply: ChannelOpenHandle`; **dropping the handle without calling
|
`reply: ChannelOpenHandle`; **dropping the handle without calling
|
||||||
`reply.accept()`/`reply.reject()` auto-rejects the open** — a
|
`reply.accept()`/`reply.reject()` auto-rejects the open**, a
|
||||||
natural fail-closed default): `channel_open_session`,
|
natural fail-closed default): `channel_open_session`,
|
||||||
`channel_open_direct_tcpip(host_to_connect, port_to_connect,
|
`channel_open_direct_tcpip(host_to_connect, port_to_connect,
|
||||||
originator_address, originator_port)`,
|
originator_address, originator_port)`,
|
||||||
@@ -119,12 +119,12 @@ inventory:
|
|||||||
`lookup_dh_gex_group`.
|
`lookup_dh_gex_group`.
|
||||||
|
|
||||||
**Dispatch/runtime facts (POC-3, load-bearing for the adapter):** the
|
**Dispatch/runtime facts (POC-3, load-bearing for the adapter):** the
|
||||||
session's event loop invokes handler callbacks *inline* — a blocking
|
session's event loop invokes handler callbacks *inline*: a blocking
|
||||||
handler suspends all packet routing for the connection (channel data
|
handler suspends all packet routing for the connection (channel data
|
||||||
is only routed by that same loop; an inline serve deadlocks against
|
is only routed by that same loop; an inline serve deadlocks against
|
||||||
a peer's follow-up bytes, e.g. a receive-pack push). Every serving
|
a peer's follow-up bytes, e.g. a receive-pack push). Every serving
|
||||||
path must `tokio::spawn` and return fast. Exit reporting from spawned
|
path must `tokio::spawn` and return fast. Exit reporting from spawned
|
||||||
tasks rides `Session::handle()` — a cloneable server-side `Handle`
|
tasks rides `Session::handle()`, a cloneable server-side `Handle`
|
||||||
with `data` / `extended_data(channel, ext, data)` / `eof` / `close` /
|
with `data` / `extended_data(channel, ext, data)` / `eof` / `close` /
|
||||||
`exit_status_request` / `exit_signal_request` (send `exit-status`
|
`exit_status_request` / `exit_signal_request` (send `exit-status`
|
||||||
before `close`). Client-side: `Channel::exec()` (no `request_exec`),
|
before `close`). Client-side: `Channel::exec()` (no `request_exec`),
|
||||||
@@ -138,14 +138,14 @@ explicitly. Details and wire evidence:
|
|||||||
|
|
||||||
### `server::Config` knobs (bounded-resources posture)
|
### `server::Config` knobs (bounded-resources posture)
|
||||||
|
|
||||||
`server_id`, `methods: MethodSet` (advertised auth methods — honest
|
`server_id`, `methods: MethodSet` (advertised auth methods; honest
|
||||||
advertisement means this must mirror the feature graph), `keys: Vec<PrivateKey>` (host keys), `certificates: Vec<Certificate>`,
|
advertisement means this must mirror the feature graph), `keys: Vec<PrivateKey>` (host keys), `certificates: Vec<Certificate>`,
|
||||||
`auth_rejection_time` + `auth_rejection_time_initial`,
|
`auth_rejection_time` + `auth_rejection_time_initial`,
|
||||||
`max_auth_attempts`, `limits: Limits` (bytes/time before rekey),
|
`max_auth_attempts`, `limits: Limits` (bytes/time before rekey),
|
||||||
`window_size`, `maximum_packet_size`, `channel_buffer_size`,
|
`window_size`, `maximum_packet_size`, `channel_buffer_size`,
|
||||||
`event_buffer_size`, `inactivity_timeout`, `keepalive_interval`,
|
`event_buffer_size`, `inactivity_timeout`, `keepalive_interval`,
|
||||||
`keepalive_max`, `nodelay`, `preferred: Preferred` (algorithm
|
`keepalive_max`, `nodelay`, `preferred: Preferred` (algorithm
|
||||||
preferences — the place any algorithm restriction would be enforced).
|
preferences, the place any algorithm restriction would be enforced).
|
||||||
|
|
||||||
Most family bounded-resource conventions map directly: max concurrent
|
Most family bounded-resource conventions map directly: max concurrent
|
||||||
channels is additionally ours (alkcall-side per-identity policy when
|
channels is additionally ours (alkcall-side per-identity policy when
|
||||||
@@ -158,14 +158,14 @@ bridged; in-crate when serving locally), session timeout =
|
|||||||
|
|
||||||
Same shape as server: `type Error: From<russh::Error> + Send + Debug`;
|
Same shape as server: `type Error: From<russh::Error> + Send + Debug`;
|
||||||
provided methods default sensibly, but **`check_server_key` defaults
|
provided methods default sensibly, but **`check_server_key` defaults
|
||||||
to reject-all and must be overridden** — host-key policy is a phase 1
|
to reject-all and must be overridden**; host-key policy is a phase 1
|
||||||
design point (for the family client: pin the expected server
|
design point (for the family client: pin the expected server
|
||||||
fingerprint at the assembly layer, per the no-trust-on-first-use
|
fingerprint at the assembly layer, per the no-trust-on-first-use
|
||||||
direction this implies). Inventory: `check_server_key`
|
direction this implies). Inventory: `check_server_key`
|
||||||
(&PublicKeyOrCertificate) → bool; `kex_done`; the full
|
(&PublicKeyOrCertificate) → bool; `kex_done`; the full
|
||||||
server-initiated channel-open set (`server_channel_open_session /
|
server-initiated channel-open set (`server_channel_open_session /
|
||||||
_direct_tcpip / _forwarded_tcpip / _forwarded_streamlocal / _x11 /
|
_direct_tcpip / _forwarded_tcpip / _forwarded_streamlocal / _x11 /
|
||||||
_agent_forward / _unknown` — the client is an acceptor for forwarded
|
_agent_forward / _unknown`; the client is an acceptor for forwarded
|
||||||
channels, i.e. the `-R` data path) with the same
|
channels, i.e. the `-R` data path) with the same
|
||||||
`ChannelOpenHandle` accept/reject semantics; `should_accept_unknown_server_channel`; data plane (`data`,
|
`ChannelOpenHandle` accept/reject semantics; `should_accept_unknown_server_channel`; data plane (`data`,
|
||||||
`extended_data`, `exit_status`, `exit_signal`, `xon_xoff`,
|
`extended_data`, `exit_status`, `exit_signal`, `xon_xoff`,
|
||||||
@@ -180,30 +180,30 @@ PrivateKeyWithHashAlg)`, `authenticate_password`,
|
|||||||
`authenticate_none`, `authenticate_keyboard_interactive_start/
|
`authenticate_none`, `authenticate_keyboard_interactive_start/
|
||||||
respond`, `authenticate_openssh_cert`, the `*_with` signer-injected
|
respond`, `authenticate_openssh_cert`, the `*_with` signer-injected
|
||||||
variants (`authenticate_publickey_with(user, PublicKey, hash_alg,
|
variants (`authenticate_publickey_with(user, PublicKey, hash_alg,
|
||||||
&mut Signer)` — a signer-injection seam; under alkssh's at-rest
|
&mut Signer)`, a signer-injection seam; under alkssh's at-rest
|
||||||
storage model (held keys are bearer-token-class secrets decrypted at
|
storage model (held keys are bearer-token-class secrets decrypted at
|
||||||
use — see `auth-bridge.md`) it is not presumed to be the vault
|
use; see `auth-bridge.md`) it is not presumed to be the vault
|
||||||
integration shape, but it exists and POC-2 evaluates it against the
|
integration shape, but it exists and POC-2 evaluates it against the
|
||||||
simple decrypt-and-decode flow; POC-2 verdict: dismissed — no
|
simple decrypt-and-decode flow; POC-2 verdict: dismissed, since no
|
||||||
provided `impl Signer` exists anywhere in 0.64.1 (the doc's "SSH
|
provided `impl Signer` exists anywhere in 0.64.1 (the doc's "SSH
|
||||||
agent" implementation claim is stale), a custom signer must
|
agent" implementation claim is stale), a custom signer must
|
||||||
replicate the splice convention (`to_sign || string{sig}`), and an
|
replicate the splice convention (`to_sign || string{sig}`), and an
|
||||||
in-process vault gains nothing over one-time decode at dial —
|
in-process vault gains nothing over one-time decode at dial; see
|
||||||
see `poc-2-vault-keys.md` §4), `authenticate_gssapi_with_mic`,
|
`poc-2-vault-keys.md` §4), `authenticate_gssapi_with_mic`,
|
||||||
`best_supported_rsa_hash` (waits ≤1s for EXT_INFO `server-sig-algs`).
|
`best_supported_rsa_hash` (waits ≤1s for EXT_INFO `server-sig-algs`).
|
||||||
|
|
||||||
Channel opening: only the four typed opens exist —
|
Channel opening: only the four typed opens exist:
|
||||||
`channel_open_session`, `channel_open_x11`,
|
`channel_open_session`, `channel_open_x11`,
|
||||||
`channel_open_direct_tcpip(host, port, originator...)`,
|
`channel_open_direct_tcpip(host, port, originator...)`,
|
||||||
`channel_open_direct_streamlocal(socket_path)` — no generic
|
`channel_open_direct_streamlocal(socket_path)`; there is no generic
|
||||||
`channel_open(name)`. On a server **reject**, the typed open call
|
`channel_open(name)`. On a server **reject**, the typed open call
|
||||||
itself resolves to `Err(Error::ChannelOpenFailure(reason))` — no
|
itself resolves to `Err(Error::ChannelOpenFailure(reason))`, and no
|
||||||
`wait()`-and-match pattern needed (POC-1 verified with vanilla
|
`wait()`-and-match pattern needed (POC-1 verified with vanilla
|
||||||
declines); the session stays usable after declines. Channels expose
|
declines); the session stays usable after declines. Channels expose
|
||||||
`wait() -> Option<ChannelMsg>`,
|
`wait() -> Option<ChannelMsg>`,
|
||||||
`split()`, `into_stream() -> ChannelStream` (AsyncRead + AsyncWrite),
|
`split()`, `into_stream() -> ChannelStream` (AsyncRead + AsyncWrite),
|
||||||
`make_reader()/make_reader_ext(ext)/make_writer()/make_writer_ext()`
|
`make_reader()/make_reader_ext(ext)/make_writer()/make_writer_ext()`
|
||||||
(tokio duplex adapters — **the bridge primitives for every mapping**),
|
(tokio duplex adapters, **the bridge primitives for every mapping**),
|
||||||
`request_pty/request_shell/exec/request_subsystem/set_env/
|
`request_pty/request_shell/exec/request_subsystem/set_env/
|
||||||
window_change/signal/eof/close/exit_status`,
|
window_change/signal/eof/close/exit_status`,
|
||||||
`writable_packet_size()`.
|
`writable_packet_size()`.
|
||||||
@@ -226,7 +226,7 @@ plus `PrivateKey`, `PublicKey`, `Certificate`, `HashAlg`,
|
|||||||
(`load_secret_key`, `decode_secret_key`, `load_public_key`,
|
(`load_secret_key`, `decode_secret_key`, `load_public_key`,
|
||||||
`parse_public_key_base64`, `load_openssh_certificate`,
|
`parse_public_key_base64`, `load_openssh_certificate`,
|
||||||
`decode_openssh`). Fingerprint computation lives in `ssh-key`
|
`decode_openssh`). Fingerprint computation lives in `ssh-key`
|
||||||
(`PublicKey::fingerprint`, SHA-256 OpenSSH format `SHA256:base64`) —
|
(`PublicKey::fingerprint`, SHA-256 OpenSSH format `SHA256:base64`),
|
||||||
relevant to `auth-bridge.md`. `known_hosts::check_known_hosts[_path]`
|
relevant to `auth-bridge.md`. `known_hosts::check_known_hosts[_path]`
|
||||||
exists for host-key verification. OpenSSH agent interop (`keys::agent`)
|
exists for host-key verification. OpenSSH agent interop (`keys::agent`)
|
||||||
exists but is out of family scope (agent forwarding declines; see
|
exists but is out of family scope (agent forwarding declines; see
|
||||||
@@ -240,20 +240,20 @@ items below are the questions as phase 0 posed them:
|
|||||||
1. Which family/russh line (current stable 0.64.1 is the default
|
1. Which family/russh line (current stable 0.64.1 is the default
|
||||||
candidate) and whether the RC-pinned `ssh-key`/`rsa`/`pkcs1` tree
|
candidate) and whether the RC-pinned `ssh-key`/`rsa`/`pkcs1` tree
|
||||||
is acceptable or gates the pin.
|
is acceptable or gates the pin.
|
||||||
2. Crypto backend (`aws-lc-rs` vs `ring`) — align with alktls.
|
2. Crypto backend (`aws-lc-rs` vs `ring`); align with alktls.
|
||||||
3. `rust-version` bump in our manifest (≥ 1.89) — lands with the
|
3. `rust-version` bump in our manifest (≥ 1.89), landing with the
|
||||||
dependency.
|
dependency.
|
||||||
4. Whether to enable the `rsa` feature at all (family keys are
|
4. Whether to enable the `rsa` feature at all (family keys are
|
||||||
ed25519-first; disabling `rsa` drops two RC deps from the tree).
|
ed25519-first; disabling `rsa` drops two RC deps from the tree).
|
||||||
5. Confirm compatibility with the vanilla clients alkforge needs (ssh
|
5. Confirm compatibility with the vanilla clients alkforge needs (ssh
|
||||||
+ git + scp-era behavior) — POC-1's deliverable.
|
+ git + scp-era behavior); POC-1's deliverable.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- docs.rs russh 0.64.1: server `Handler`, client `Handler`, `Config`s,
|
- docs.rs russh 0.64.1: server `Handler`, client `Handler`, `Config`s,
|
||||||
`Handle`, `keys` module (sections above quote their inventories)
|
`Handle`, `keys` module (sections above quote their inventories)
|
||||||
- russh GitHub releases 0.64.0 / 0.64.1 (GHSA ids quoted above)
|
- russh GitHub releases 0.64.0 / 0.64.1 (GHSA ids quoted above)
|
||||||
- alknet `main` `crates/alknet-core/src/auth/` — rough reference only,
|
- alknet `main` `crates/alknet-core/src/auth/`: rough reference only,
|
||||||
explicitly not decision input (0.49-era API; verify nothing against
|
explicitly not decision input (0.49-era API; verify nothing against
|
||||||
it)
|
it)
|
||||||
|
|
||||||
@@ -265,10 +265,10 @@ build (`/tmp/opencode/alkssh-pin-check`, disposable):
|
|||||||
|
|
||||||
| Fact | Verified value |
|
| Fact | Verified value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Current published | **0.65.0** (2026-10-25) — see Version landscape above |
|
| Current published | **0.65.0** (2026-10-25); see Version landscape above |
|
||||||
| Backend feature | `aws-lc-rs` (dashed feature name; also russh's own default alongside `flate2`, `rsa`) |
|
| Backend feature | `aws-lc-rs` (dashed feature name; also russh's own default alongside `flate2`, `rsa`) |
|
||||||
| Scratch build | `default-features = false, features = ["aws-lc-rs", "rsa"]` — clean, ~50 s, **no external provisioning** (prebuilt aws-lc bindings cover this platform; `aws-lc-rs 1.18.2` / `aws-lc-sys 0.46.0`) |
|
| Scratch build | `default-features = false, features = ["aws-lc-rs", "rsa"]`: clean, ~50 s, **no external provisioning** (prebuilt aws-lc bindings cover this platform; `aws-lc-rs 1.18.2` / `aws-lc-sys 0.46.0`) |
|
||||||
| Tree (the pin set) | `ssh-key =0.7.0-rc.11` (exact), `rsa =0.10.0-rc.18`, `pkcs1 =0.8.0-rc.4`, `getrandom 0.4.3`, `rand_core 0.10.1` — same RC set as the POCs' 0.64.1 tree |
|
| Tree (the pin set) | `ssh-key =0.7.0-rc.11` (exact), `rsa =0.10.0-rc.18`, `pkcs1 =0.8.0-rc.4`, `getrandom 0.4.3`, `rand_core 0.10.1`; same RC set as the POCs' 0.64.1 tree |
|
||||||
| MSRV | 1.89 (both 0.64.1 and 0.65.0) |
|
| MSRV | 1.89 (both 0.64.1 and 0.65.0) |
|
||||||
| License | Apache-2.0 only |
|
| License | Apache-2.0 only |
|
||||||
|
|
||||||
|
|||||||
+20
-21
@@ -6,8 +6,8 @@ Status: Draft (phase 0 research).
|
|||||||
|
|
||||||
alkssh is the alk family's SSH adapter: a thin wrapper around russh
|
alkssh is the alk family's SSH adapter: a thin wrapper around russh
|
||||||
terminating the vanilla-ssh wire and bridging to/from alkcall channels
|
terminating the vanilla-ssh wire and bridging to/from alkcall channels
|
||||||
(family ADR-003). It is the alkhttp-style adapter for the SSH protocol
|
(family ADR-003). It is the alkhttp-style adapter for the SSH protocol, the
|
||||||
— the vanilla-ssh front door for vanilla clients, and the family's
|
vanilla-ssh front door for vanilla clients, and the family's
|
||||||
vanilla-ssh client where a family app dials an upstream SSH server.
|
vanilla-ssh client where a family app dials an upstream SSH server.
|
||||||
|
|
||||||
The substance almost entirely exists in the family. What alkssh adds is
|
The substance almost entirely exists in the family. What alkssh adds is
|
||||||
@@ -25,7 +25,7 @@ The alkforge-relevant property is **exclusion**: an alkforge ssh
|
|||||||
endpoint built with only the `git` feature is structurally incapable of
|
endpoint built with only the `git` feature is structurally incapable of
|
||||||
serving a shell, tunnels, or socks. Every mapping is a cargo feature;
|
serving a shell, tunnels, or socks. Every mapping is a cargo feature;
|
||||||
unfeatured channel opens decline fail-closed (family ADR-003 decision
|
unfeatured channel opens decline fail-closed (family ADR-003 decision
|
||||||
3). This is why alkssh leads alkforge's phase 0 — the adapter is glue,
|
3). This is why alkssh leads alkforge's phase 0: the adapter is glue,
|
||||||
not protocol invention.
|
not protocol invention.
|
||||||
|
|
||||||
## Guiding principles
|
## Guiding principles
|
||||||
@@ -36,10 +36,10 @@ not protocol invention.
|
|||||||
party-upstream posture applies (file asks upstream; never silently
|
party-upstream posture applies (file asks upstream; never silently
|
||||||
absorb a workaround).
|
absorb a workaround).
|
||||||
2. **A door in both directions, vanilla on the outside.** alkssh is a
|
2. **A door in both directions, vanilla on the outside.** alkssh is a
|
||||||
door for vanilla clients (server direction: vanilla in, family out
|
door for vanilla clients (server direction: vanilla in, family
|
||||||
— each channel served by a family seam entry; primary current
|
out, each channel served by a family seam entry; primary current
|
||||||
purpose: git) and a door to vanilla servers (client direction:
|
purpose: git) and a door to vanilla servers (client direction:
|
||||||
family in, vanilla out — the reverse-runner shape: a family hub
|
family in, vanilla out; the reverse-runner shape: a family hub
|
||||||
dials vanilla sshd and the ops map family-side onto the same
|
dials vanilla sshd and the ops map family-side onto the same
|
||||||
protocols, **alktty** for sessions, **alktunnels** for
|
protocols, **alktty** for sessions, **alktunnels** for
|
||||||
tunnels/-D legs, **alkgit** for upstream git, alkfs client later
|
tunnels/-D legs, **alkgit** for upstream git, alkfs client later
|
||||||
@@ -64,19 +64,19 @@ not protocol invention.
|
|||||||
conventions, and password auth are not the alk family's identity
|
conventions, and password auth are not the alk family's identity
|
||||||
model. A verified key's fingerprint → `Identity` via alkcall's
|
model. A verified key's fingerprint → `Identity` via alkcall's
|
||||||
`IdentityProvider` is the only resolution path; secrets an alkssh
|
`IdentityProvider` is the only resolution path; secrets an alkssh
|
||||||
*client* holds come from alkvault-backed sources — vault-encrypted
|
*client* holds come from alkvault-backed sources: vault-encrypted
|
||||||
at rest, treated as bearer-token-class secret material — never env
|
at rest, treated as bearer-token-class secret material, never env
|
||||||
vars (family ADR-002 decision 5). The vault is a secret store on
|
vars (family ADR-002 decision 5). The vault is a secret store on
|
||||||
this path, not a key generator or key custodian.
|
this path, not a key generator or key custodian.
|
||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- **Authoring SSH protocol machinery** — no in-family framing, cipher,
|
- **Authoring SSH protocol machinery**: no in-family framing, cipher,
|
||||||
or KEX code, ever (russh upstream decides what SSH is).
|
or KEX code, ever (russh upstream decides what SSH is).
|
||||||
- **Being an sshd.** No local user accounts, no `authorized_keys` file
|
- **Being an sshd.** No local user accounts, no `authorized_keys` file
|
||||||
semantics of the OpenSSH variety, no PAM-shaped anything. Deployment
|
semantics of the OpenSSH variety, no PAM-shaped anything. Deployment
|
||||||
posture belongs to the embedding app.
|
posture belongs to the embedding app.
|
||||||
- **Serving sftp** — deferred to alkfs (family ADR-002); the subsystem
|
- **Serving sftp**: deferred to alkfs (family ADR-002); the subsystem
|
||||||
decline is the placeholder contract.
|
decline is the placeholder contract.
|
||||||
- **A binary / CLI.** Downstream apps embed the crate; there is no
|
- **A binary / CLI.** Downstream apps embed the crate; there is no
|
||||||
`alkssh` executable in this crate.
|
`alkssh` executable in this crate.
|
||||||
@@ -90,26 +90,25 @@ not protocol invention.
|
|||||||
These frame the research docs; each is a candidate open question for
|
These frame the research docs; each is a candidate open question for
|
||||||
phase 1's `open-questions.md`:
|
phase 1's `open-questions.md`:
|
||||||
|
|
||||||
1. **The `alk/ssh` ALPN's topology role** — alkcall ADR-004 reserves
|
1. **The `alk/ssh` ALPN's topology role**: alkcall ADR-004 reserves
|
||||||
`alk/ssh` → `SshAdapter`, ADR-034's channels table gives it
|
`alk/ssh` → `SshAdapter`, ADR-034's channels table gives it
|
||||||
stream_types `[0, 1]` ("SSH multiplexes internally, including its
|
stream_types `[0, 1]` ("SSH multiplexes internally, including its
|
||||||
own control"), and ADR-009 sketches a browser-WASM route where the
|
own control"), and ADR-009 sketches a browser-WASM route where the
|
||||||
ALPN transports an SSH stream to an `SshAdapter`. But family ADR-003
|
ALPN transports an SSH stream to an `SshAdapter`. But family ADR-003
|
||||||
describes alkssh terminating vanilla SSH at an edge. Whether
|
describes alkssh terminating vanilla SSH at an edge. It remains
|
||||||
`alk/ssh` carries raw vanilla-SSH bytes between alkssh instances
|
unresolved whether `alk/ssh` carries raw vanilla-SSH bytes between
|
||||||
(a relay leg), serves the browser route, or both — and how that
|
alkssh instances (a relay leg), serves the browser route, or both,
|
||||||
interacts with ADR-042's "the hub never runs a handler for
|
and how that interacts with ADR-042's "the hub never runs a handler
|
||||||
`alk/ssh`" relay ruling — is unresolved (see `mapping-table.md`
|
for `alk/ssh`" relay ruling (see `mapping-table.md` §topology).
|
||||||
§topology).
|
2. **russh pin**: version, crypto-backend feature, and the
|
||||||
2. **russh pin** — version, crypto-backend feature, and the
|
|
||||||
RC-pinned transitive dependency situation (see `russh.md`).
|
RC-pinned transitive dependency situation (see `russh.md`).
|
||||||
3. **Feature names + graph** — the `session`/`tunnel`/`dynamic-forward`/`git`/`sftp` naming and their independence
|
3. **Feature names + graph**: the `session`/`tunnel`/`dynamic-forward`/`git`/`sftp` naming and their independence
|
||||||
(no feature implies another; flat until architecture says otherwise).
|
(no feature implies another; flat until architecture says otherwise).
|
||||||
4. **Auth dispositions** — which russh auth methods the server
|
4. **Auth dispositions**: which russh auth methods the server
|
||||||
implements vs declines; the exact `auth_publickey` → fingerprint →
|
implements vs declines; the exact `auth_publickey` → fingerprint →
|
||||||
identity contract; client-side vault-backed at-rest key handling
|
identity contract; client-side vault-backed at-rest key handling
|
||||||
(see `auth-bridge.md`).
|
(see `auth-bridge.md`).
|
||||||
5. **The mapping table details** — per channel type and per request,
|
5. **The mapping table details**: per channel type and per request,
|
||||||
the family seam, the parameter mapping, and the decline contract
|
the family seam, the parameter mapping, and the decline contract
|
||||||
(see `mapping-table.md`); POC validation per `pocs.md`.
|
(see `mapping-table.md`); POC validation per `pocs.md`.
|
||||||
|
|
||||||
|
|||||||
+43
-42
@@ -18,7 +18,7 @@ leverages:
|
|||||||
4. **Task-Driven**: Structured task graphs with dependency analysis
|
4. **Task-Driven**: Structured task graphs with dependency analysis
|
||||||
5. **Safe Exit**: Always have a way to unblock progress when stuck
|
5. **Safe Exit**: Always have a way to unblock progress when stuck
|
||||||
6. **Categorical Estimates**: Use risk/scope/impact categories, not time
|
6. **Categorical Estimates**: Use risk/scope/impact categories, not time
|
||||||
estimates. These are structurally important — upstream failures multiply
|
estimates. These are structurally important: upstream failures multiply
|
||||||
downstream damage regardless of developer type (human or LLM). See the
|
downstream damage regardless of developer type (human or LLM). See the
|
||||||
cost-benefit framework in taskgraph's framework docs.
|
cost-benefit framework in taskgraph's framework docs.
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ problems need investigation.
|
|||||||
4. Document learnings
|
4. Document learnings
|
||||||
5. Converge on recommended approach
|
5. Converge on recommended approach
|
||||||
|
|
||||||
**POC placement (two modes)** — POC code never merges to main; the
|
**POC placement (two modes)**: POC code never merges to main; the
|
||||||
deliverable is the findings write-up:
|
deliverable is the findings write-up:
|
||||||
|
|
||||||
- **Branch mode** (POC builds on code in this repo): git branch off main,
|
- **Branch mode** (POC builds on code in this repo): git branch off main,
|
||||||
@@ -56,11 +56,11 @@ approaches
|
|||||||
**Process**:
|
**Process**:
|
||||||
|
|
||||||
1. Architect creates the architecture documentation structure:
|
1. Architect creates the architecture documentation structure:
|
||||||
- `docs/architecture/README.md` — index with doc table, ADR table, lifecycle
|
- `docs/architecture/README.md`: index with doc table, ADR table, lifecycle
|
||||||
- `docs/architecture/overview.md` — package purpose, exports, dependencies
|
- `docs/architecture/overview.md`: package purpose, exports, dependencies
|
||||||
- `docs/architecture/<component>.md` — one focused doc per component/area
|
- `docs/architecture/<component>.md`: one focused doc per component/area
|
||||||
- `docs/architecture/decisions/` — numbered ADR files (ADR-001, ADR-002, ...)
|
- `docs/architecture/decisions/`: numbered ADR files (ADR-001, ADR-002, ...)
|
||||||
- `docs/architecture/open-questions.md` — centralized tracker with OQ-IDs
|
- `docs/architecture/open-questions.md`: centralized tracker with OQ-IDs
|
||||||
2. All docs start in `Draft` status. Spec docs reference ADRs by number (not
|
2. All docs start in `Draft` status. Spec docs reference ADRs by number (not
|
||||||
inline rationale) and OQs by number (not inline questions).
|
inline rationale) and OQs by number (not inline questions).
|
||||||
3. Architecture Review validates for ambiguities, risks, and structural issues
|
3. Architecture Review validates for ambiguities, risks, and structural issues
|
||||||
@@ -146,16 +146,16 @@ WHAT, ADRs explain WHY, open questions track what's unresolved.
|
|||||||
- Extract decisions into numbered ADRs in `decisions/` directory
|
- Extract decisions into numbered ADRs in `decisions/` directory
|
||||||
- Centralize open questions in `open-questions.md` with OQ-IDs
|
- Centralize open questions in `open-questions.md` with OQ-IDs
|
||||||
- Maintain `README.md` as the architecture index
|
- Maintain `README.md` as the architecture index
|
||||||
- Keep spec documents focused (~500 lines) — reference ADRs and OQs, don't inline them
|
- Keep spec documents focused (about 500 lines); reference ADRs and OQs, don't inline them
|
||||||
- Redirect exploration work to Research Specialist
|
- Redirect exploration work to Research Specialist
|
||||||
- Iterate based on review feedback
|
- Iterate based on review feedback
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- `docs/architecture/README.md` — index with doc table, ADR table, lifecycle
|
- `docs/architecture/README.md`: index with doc table, ADR table, lifecycle
|
||||||
- `docs/architecture/<component>.md` — focused spec documents
|
- `docs/architecture/<component>.md`: focused spec documents
|
||||||
- `docs/architecture/decisions/` — numbered ADR files
|
- `docs/architecture/decisions/`: numbered ADR files
|
||||||
- `docs/architecture/open-questions.md` — centralized OQ tracker
|
- `docs/architecture/open-questions.md`: centralized OQ tracker
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -191,17 +191,17 @@ sessions.
|
|||||||
**Mode**: Primary (manages worktrees and agent sessions)
|
**Mode**: Primary (manages worktrees and agent sessions)
|
||||||
|
|
||||||
**Uses**: The `worktree` tool from the **open-coordinator** opencode plugin.
|
**Uses**: The `worktree` tool from the **open-coordinator** opencode plugin.
|
||||||
Single tool with `{action, args}` dispatch. Role is auto-detected — coordinator
|
Single tool with `{action, args}` dispatch. Role is auto-detected: coordinator
|
||||||
sessions get the full operation set, spawned implementation sessions get a
|
sessions get the full operation set, spawned implementation sessions get a
|
||||||
limited set (current, notify, status). No mode toggle required.
|
limited set (current, notify, status). No mode toggle required.
|
||||||
|
|
||||||
**Tools**:
|
**Tools**:
|
||||||
|
|
||||||
- `worktree({action, args})` — spawn, sessions, dashboard, message, abort,
|
- `worktree({action, args})`: spawn, sessions, dashboard, message, abort,
|
||||||
cleanup
|
cleanup
|
||||||
- Bash (opencode CLI for session interaction)
|
- Bash (opencode CLI for session interaction)
|
||||||
- Read (monitor task files)
|
- Read (monitor task files)
|
||||||
- `memory` / `memory_compact` — context management and session history (via
|
- `memory` / `memory_compact`: context management and session history (via
|
||||||
@alkdev/open-memory, when available)
|
@alkdev/open-memory, when available)
|
||||||
|
|
||||||
**Key Behaviors**:
|
**Key Behaviors**:
|
||||||
@@ -230,10 +230,10 @@ limited set (current, notify, status). No mode toggle required.
|
|||||||
**Tools**:
|
**Tools**:
|
||||||
|
|
||||||
- Read, Write, Edit, Glob, Grep, Bash
|
- Read, Write, Edit, Glob, Grep, Bash
|
||||||
- `worktree({action: "notify", ...})` — report progress/blockers to coordinator
|
- `worktree({action: "notify", ...})`: report progress/blockers to coordinator
|
||||||
- `worktree({action: "current"})` — verify worktree assignment
|
- `worktree({action: "current"})`: verify worktree assignment
|
||||||
- webSearch (documentation lookup)
|
- webSearch (documentation lookup)
|
||||||
- `memory` / `memory_compact` — context management (via @alkdev/open-memory,
|
- `memory` / `memory_compact`: context management (via @alkdev/open-memory,
|
||||||
when available)
|
when available)
|
||||||
|
|
||||||
**Key Behaviors**:
|
**Key Behaviors**:
|
||||||
@@ -340,8 +340,8 @@ mode: scratch project outside the repo)
|
|||||||
- Create minimal POCs to validate hypotheses
|
- Create minimal POCs to validate hypotheses
|
||||||
- Branch mode: work in isolated research worktrees; standalone mode: work
|
- Branch mode: work in isolated research worktrees; standalone mode: work
|
||||||
in the scratch project only
|
in the scratch project only
|
||||||
- Document findings and recommendations (findings are the deliverable —
|
- Document findings and recommendations (findings are the deliverable; POC
|
||||||
POC source is disposable and never merges to main)
|
source is disposable and never merges to main)
|
||||||
- Timebox strictly - abandon if taking too long
|
- Timebox strictly - abandon if taking too long
|
||||||
- Be honest about limitations and blockers
|
- Be honest about limitations and blockers
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ Implement OAuth2 authentication with provider abstraction.
|
|||||||
### Categorical Estimates
|
### Categorical Estimates
|
||||||
|
|
||||||
These fields are structurally important, not optional metadata. They power
|
These fields are structurally important, not optional metadata. They power
|
||||||
`taskgraph decompose`, `risk-path`, `critical`, and `bottleneck` — commands that
|
`taskgraph decompose`, `risk-path`, `critical`, and `bottleneck`: commands that
|
||||||
reveal structural problems in the task graph. A task missing `scope`, `risk`,
|
reveal structural problems in the task graph. A task missing `scope`, `risk`,
|
||||||
`impact`, or `level` is a red flag indicating incomplete decomposition. See the
|
`impact`, or `level` is a red flag indicating incomplete decomposition. See the
|
||||||
cost-benefit framework in taskgraph's framework docs for the reasoning.
|
cost-benefit framework in taskgraph's framework docs for the reasoning.
|
||||||
@@ -432,7 +432,7 @@ cost-benefit framework in taskgraph's framework docs for the reasoning.
|
|||||||
| phase | A whole phase's milestone or gate slips (e.g. a decomposition OQ)|
|
| phase | A whole phase's milestone or gate slips (e.g. a decomposition OQ)|
|
||||||
| project | The project's contract: wire formats, published API, security |
|
| project | The project's contract: wire formats, published API, security |
|
||||||
|
|
||||||
**Valid frontmatter enum variants (taskgraph 0.1.3)** — only these
|
**Valid frontmatter enum variants (taskgraph 0.1.3)**: only these
|
||||||
literal values parse; anything else fails `taskgraph validate`:
|
literal values parse; anything else fails `taskgraph validate`:
|
||||||
|
|
||||||
- `status`: `pending` | `in-progress` | `completed` | `failed` | `blocked`
|
- `status`: `pending` | `in-progress` | `completed` | `failed` | `blocked`
|
||||||
@@ -443,20 +443,21 @@ literal values parse; anything else fails `taskgraph validate`:
|
|||||||
|
|
||||||
Three gotchas worth stating because agents have hit all three:
|
Three gotchas worth stating because agents have hit all three:
|
||||||
|
|
||||||
1. `status: done` is **not** a value — the lifecycle word is
|
1. `status: done` is **not** a value; the lifecycle word is
|
||||||
`completed` (and `in-progress`, not `in_progress` or `wip`).
|
`completed` (and `in-progress`, not `in_progress` or `wip`).
|
||||||
2. A file in `tasks/` **without frontmatter is silently skipped** by
|
2. A file in `tasks/` **without frontmatter is silently skipped** by
|
||||||
`taskgraph validate`, `list`, and every graph command — no error,
|
`taskgraph validate`, `list`, and every graph command. It produces
|
||||||
the file just doesn't exist as far as the graph is concerned.
|
no error; the file just doesn't exist as far as the graph is
|
||||||
|
concerned.
|
||||||
Every tracker or task file under `tasks/` must carry a complete
|
Every tracker or task file under `tasks/` must carry a complete
|
||||||
frontmatter block (`id`, `name` required; `depends_on` plus the
|
frontmatter block (`id`, `name` required; `depends_on` plus the
|
||||||
five categorical fields expected). If you need a plain prose
|
five categorical fields expected). If you need a plain prose
|
||||||
tracker, keep it under `docs/` instead.
|
tracker, keep it under `docs/` instead.
|
||||||
3. `status` transitions run through the lifecycle only: `pending` →
|
3. `status` transitions run through the lifecycle only: `pending` →
|
||||||
`in-progress` → `completed` (or `failed`/`blocked`). A finished
|
`in-progress` → `completed` (or `failed`/`blocked`). A finished
|
||||||
task never goes sideways to `done`/`finished` — the enum is closed.
|
task never goes sideways to `done`/`finished`; the enum is closed.
|
||||||
|
|
||||||
When in doubt, run `taskgraph validate` — it is the gate that CI-style
|
When in doubt, run `taskgraph validate`: it is the gate that CI-style
|
||||||
checks and review passes will hit.
|
checks and review passes will hit.
|
||||||
|
|
||||||
### Task Lifecycle
|
### Task Lifecycle
|
||||||
@@ -472,15 +473,15 @@ checks and review passes will hit.
|
|||||||
|
|
||||||
### Architecture work and the `level` field
|
### Architecture work and the `level` field
|
||||||
|
|
||||||
Architecture work uses the same taskgraph format as implementation work —
|
Architecture work uses the same taskgraph format as implementation work;
|
||||||
there is no separate `architecture` level. The `level` enum
|
there is no separate `architecture` level. The `level` enum
|
||||||
(`planning, decomposition, implementation, review, research`) already covers
|
(`planning, decomposition, implementation, review, research`) already covers
|
||||||
architecture activities:
|
architecture activities:
|
||||||
|
|
||||||
| Architecture activity | `level` | Rationale |
|
| Architecture activity | `level` | Rationale |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Writing an ADR (the decision) | `implementation` | The ADR *is* the deliverable — the implementation of an architecture decision. Calling it `planning` would mislabel the artifact. |
|
| Writing an ADR (the decision) | `implementation` | The ADR *is* the deliverable, namely the implementation of an architecture decision. Calling it `planning` would mislabel the artifact. |
|
||||||
| Decomposing a spec into ADRs + OQs | `decomposition` | Direct fit — breaking a large architecture area into decideable units. |
|
| Decomposing a spec into ADRs + OQs | `decomposition` | Direct fit: breaking a large architecture area into decideable units. |
|
||||||
| Architecture review pass | `review` | Direct fit. |
|
| Architecture review pass | `review` | Direct fit. |
|
||||||
| Research / POC before a decision | `research` | Direct fit. |
|
| Research / POC before a decision | `research` | Direct fit. |
|
||||||
| Spec writing (overview, component spec) | `implementation` | The spec is the deliverable. |
|
| Spec writing (overview, component spec) | `implementation` | The spec is the deliverable. |
|
||||||
@@ -530,8 +531,8 @@ the deferral has two halves that must stay in sync:
|
|||||||
2. **The machine-readable half** (in `tasks/architecture/`): an
|
2. **The machine-readable half** (in `tasks/architecture/`): an
|
||||||
external-trigger tracker task represents the unblocking condition. It is
|
external-trigger tracker task represents the unblocking condition. It is
|
||||||
tagged `[external-trigger, deferred-oq]`, has `risk: trivial` and
|
tagged `[external-trigger, deferred-oq]`, has `risk: trivial` and
|
||||||
`level: research` (it is not actionable work — it tracks whether the
|
`level: research` (it is not actionable work; it tracks whether the
|
||||||
external condition has arrived), and its `id` is referenced from the OQ's
|
external condition has arrived), and its `id` is referenced from the OQ's
|
||||||
`Blocked on:` text. This is the surface for the task graph: `taskgraph`
|
`Blocked on:` text. This is the surface for the task graph: `taskgraph`
|
||||||
tools can reason about it, and downstream work that depends on the
|
tools can reason about it, and downstream work that depends on the
|
||||||
decision can declare `depends_on: [architecture/oq-NN-...]`.
|
decision can declare `depends_on: [architecture/oq-NN-...]`.
|
||||||
@@ -540,7 +541,7 @@ When the unblocking condition arrives (a use case materializes, a crate is
|
|||||||
specced), the tracker task is marked `completed` and the OQ transitions from
|
specced), the tracker task is marked `completed` and the OQ transitions from
|
||||||
`deferred(scope)` to `open` (or directly to `resolved` if no architecture
|
`deferred(scope)` to `open` (or directly to `resolved` if no architecture
|
||||||
decision remains). The two halves serve different audiences and use one edge
|
decision remains). The two halves serve different audiences and use one edge
|
||||||
type (`depends_on`) — the reverse lookup uses `taskgraph dependents`. Do not
|
type (`depends_on`); the reverse lookup uses `taskgraph dependents`. Do not
|
||||||
add a `blocks:` field; it duplicates the edge and creates a sync hazard.
|
add a `blocks:` field; it duplicates the edge and creates a sync hazard.
|
||||||
|
|
||||||
## Review Injection
|
## Review Injection
|
||||||
@@ -559,8 +560,8 @@ Use graph analysis to determine where reviews should happen:
|
|||||||
### Current (open-coordinator plugin)
|
### Current (open-coordinator plugin)
|
||||||
|
|
||||||
The Coordinator uses the `worktree` tool from the open-coordinator opencode
|
The Coordinator uses the `worktree` tool from the open-coordinator opencode
|
||||||
plugin. It's a single tool with `{action, args}` dispatch — no separate
|
plugin. It's a single tool with `{action, args}` dispatch, so there are no
|
||||||
enable/toggle steps. Role is auto-detected from session state.
|
separate enable/toggle steps. Role is auto-detected from session state.
|
||||||
|
|
||||||
```
|
```
|
||||||
1. Identify parallel work
|
1. Identify parallel work
|
||||||
@@ -613,13 +614,13 @@ a worktree.
|
|||||||
When the open-memory plugin is available alongside open-coordinator, the
|
When the open-memory plugin is available alongside open-coordinator, the
|
||||||
coordinator gains:
|
coordinator gains:
|
||||||
|
|
||||||
- `memory({tool: "children", args: {sessionId: "..."}})` — view sub-agent
|
- `memory({tool: "children", args: {sessionId: "..."}})`: view sub-agent
|
||||||
sessions spawned from the coordinator
|
sessions spawned from the coordinator
|
||||||
- `memory({tool: "messages", args: {sessionId: "..."}})` — read a spawned
|
- `memory({tool: "messages", args: {sessionId: "..."}})`: read a spawned
|
||||||
session's conversation for debugging
|
session's conversation for debugging
|
||||||
- `memory({tool: "context"})` — check context window usage before long
|
- `memory({tool: "context"})`: check context window usage before long
|
||||||
monitoring sessions
|
monitoring sessions
|
||||||
- `memory_compact()` — proactively compact at natural breakpoints
|
- `memory_compact()`: proactively compact at natural breakpoints
|
||||||
|
|
||||||
Implementation agents can also use `memory({tool: "context"})` and
|
Implementation agents can also use `memory({tool: "context"})` and
|
||||||
`memory_compact()` to manage their context during long tasks.
|
`memory_compact()` to manage their context during long tasks.
|
||||||
@@ -643,7 +644,7 @@ Once the hub is operational, coordination uses native operations:
|
|||||||
```
|
```
|
||||||
|
|
||||||
State moves from in-process tracking to Postgres `mappings` table. The
|
State moves from in-process tracking to Postgres `mappings` table. The
|
||||||
open-coordinator plugin becomes unnecessary — the hub provides the same
|
open-coordinator plugin becomes unnecessary: the hub provides the same
|
||||||
capabilities as server-side operations accessible from any environment.
|
capabilities as server-side operations accessible from any environment.
|
||||||
|
|
||||||
## Document Structure
|
## Document Structure
|
||||||
@@ -710,7 +711,7 @@ Accepted | Proposed | Deprecated | Superseded
|
|||||||
(Links to related specs and ADRs)
|
(Links to related specs and ADRs)
|
||||||
```
|
```
|
||||||
|
|
||||||
Numbering starts at 001 within each project. ADRs are stable — once Accepted,
|
Numbering starts at 001 within each project. ADRs are stable: once Accepted,
|
||||||
they don't revert. If superseded, mark the old one and create a new one.
|
they don't revert. If superseded, mark the old one and create a new one.
|
||||||
|
|
||||||
### Open Questions Format
|
### Open Questions Format
|
||||||
@@ -726,7 +727,7 @@ they don't revert. If superseded, mark the old one and create a new one.
|
|||||||
- **Status**: open | resolved | deferred(scope) | partially resolved
|
- **Status**: open | resolved | deferred(scope) | partially resolved
|
||||||
- **Priority**: high | medium | low
|
- **Priority**: high | medium | low
|
||||||
- **Resolution**: (when resolved)
|
- **Resolution**: (when resolved)
|
||||||
- **Blocked on**: (when deferred — concrete dependency that would unblock)
|
- **Blocked on**: (when deferred; the concrete dependency that would unblock)
|
||||||
- **Cross-references**: OQ-NN, ADR-NNN
|
- **Cross-references**: OQ-NN, ADR-NNN
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Generated
+5
-5
@@ -109,7 +109,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alkssh"
|
name = "alkssh"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alkcall",
|
"alkcall",
|
||||||
"alktty",
|
"alktty",
|
||||||
@@ -146,9 +146,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alktty"
|
name = "alktty"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2265167842b7d3ea09bc235f48450d3dcf22049a2ca71f8155de00acbf97c226"
|
checksum = "9cab9b0d3d6c1bdef2622799e3b95bb6063720153a124a57f4c60f95f3d42ea8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alkcall",
|
"alkcall",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -166,9 +166,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alkvault"
|
name = "alkvault"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52e032b074f7fea71ea15528994c5b3a6503c082c519045bb3b0519281291918"
|
checksum = "b2522ded10a506770779e72063422057bb63f1b75457c1742a3362072adfb1c4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+16
-16
@@ -1,16 +1,16 @@
|
|||||||
# alkssh fuzzing
|
# alkssh fuzzing
|
||||||
|
|
||||||
libFuzzer targets for alkssh's ingress surfaces — the family standing
|
libFuzzer targets for alkssh's ingress surfaces: the family standing
|
||||||
rule (anything worth fuzzing gets fuzzed), landed per the plan's wave-5
|
rule (anything worth fuzzing gets fuzzed), landed per the plan's wave-5
|
||||||
charter and the alkgit convention: the JSON schema gate itself is
|
charter and the alkgit convention. The JSON schema gate itself is
|
||||||
alkcall's surface, family-fuzzed there; SSH framing, key exchange, and
|
alkcall's surface, family-fuzzed there; SSH framing, key exchange, and
|
||||||
the channel multiplexer are russh's wire (family ADR-003) — the
|
the channel multiplexer are russh's wire (family ADR-003); the
|
||||||
alkssh-owned post-framing ingress is what fuzzes here.
|
alkssh-owned post-framing ingress is what fuzzes here.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- `fuzz_targets/` — one binary per target; thin `fuzz_target!` wrappers.
|
- `fuzz_targets/`: one binary per target; thin `fuzz_target!` wrappers.
|
||||||
- `shared/` — the invariant logic, as a plain library so normal
|
- `shared/`: the invariant logic, as a plain library so normal
|
||||||
`cargo test` (stable toolchain) can replay the committed corpora
|
`cargo test` (stable toolchain) can replay the committed corpora
|
||||||
through the same invariants (`#[cfg(test)]` modules; quinn's CI
|
through the same invariants (`#[cfg(test)]` modules; quinn's CI
|
||||||
pattern). The fuzz binaries are nightly-only; the shared crate is
|
pattern). The fuzz binaries are nightly-only; the shared crate is
|
||||||
@@ -18,34 +18,34 @@ alkssh-owned post-framing ingress is what fuzzes here.
|
|||||||
`#[doc(hidden)]` fuzz-visible exposures: the exec-grammar parse path
|
`#[doc(hidden)]` fuzz-visible exposures: the exec-grammar parse path
|
||||||
in `src/dispatch.rs`, the key-decode arm `decode_key_arm` in
|
in `src/dispatch.rs`, the key-decode arm `decode_key_arm` in
|
||||||
`src/key_source.rs`, and the `terminal_params` conversion in
|
`src/key_source.rs`, and the `terminal_params` conversion in
|
||||||
`src/tty_map.rs` — the recorded exposure decisions: the ingresses are
|
`src/tty_map.rs`; the recorded exposure decisions: the ingresses are
|
||||||
production-private, so the exposures are the production surfaces
|
production-private, so the exposures are the production surfaces
|
||||||
themselves, never forks; the fingerprint ingress is fully public API)
|
themselves, never forks; the fingerprint ingress is fully public API)
|
||||||
and must never become a dependency of the root crate.
|
and must never become a dependency of the root crate.
|
||||||
- `corpus/<target>/` — committed hand-made seeds (regenerate with
|
- `corpus/<target>/`: committed hand-made seeds (regenerate with
|
||||||
`python3 fuzz/gen_fuzz_seeds.py` from the repo root — must be a no-op
|
`python3 fuzz/gen_fuzz_seeds.py` from the repo root; the result must
|
||||||
diff on the committed set). Only `seed-*` files are tracked; grown
|
be a no-op diff on the committed set). Only `seed-*` files are tracked; grown
|
||||||
corpora (SHA-named units), artifacts, and coverage output are
|
corpora (SHA-named units), artifacts, and coverage output are
|
||||||
gitignored and are **not** merged back into the committed seeds.
|
gitignored and are **not** merged back into the committed seeds.
|
||||||
- `run-detached.sh` — mandatory runner for agent sessions: wraps
|
- `run-detached.sh`: the mandatory runner for agent sessions. It wraps
|
||||||
`cargo fuzz run` in `setsid` + `nohup` + log redirection so an OOM
|
`cargo fuzz run` in `setsid` + `nohup` + log redirection so an OOM
|
||||||
in a target can never take down the agent host.
|
in a target can never take down the agent host.
|
||||||
- `gen_fuzz_seeds.py` — deterministic seed generator (quiche pattern).
|
- `gen_fuzz_seeds.py`: deterministic seed generator (quiche pattern).
|
||||||
|
|
||||||
## Targets
|
## Targets
|
||||||
|
|
||||||
| Target | Drives | Invariants |
|
| Target | Drives | Invariants |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `exec_parse` | the production exec-grammar parse (`parse_exec`, `src/dispatch.rs` — the handler's ingress shape: byte string → `from_utf8` fallback `""` → parse; the pre-parse `max_exec_command_len` cap and the git row's `invalid-repo-id` gate are the root contract rows' territory, the parse is everything behind them) | no-panic on any byte string (any length behind the cap); the typed routing classes (git-parseable → the git row, non-git → the session pipe path, service-only → decline-with-reason) agree with a from-spec grammar oracle (the differential pair) and are stable per input; for `Git` routes the repo-id gate classification is deterministic |
|
| `exec_parse` | the production exec-grammar parse (`parse_exec`, `src/dispatch.rs`, the handler's ingress shape: byte string → `from_utf8` fallback `""` → parse; the pre-parse `max_exec_command_len` cap and the git row's `invalid-repo-id` gate are the root contract rows' territory, the parse is everything behind them) | no-panic on any byte string (any length behind the cap); the typed routing classes (git-parseable → the git row, non-git → the session pipe path, service-only → decline-with-reason) agree with a from-spec grammar oracle (the differential pair) and are stable per input; for `Git` routes the repo-id gate classification is deterministic |
|
||||||
| `fingerprint` | the crate's single fingerprint path ([`alkssh::fingerprint`] over `russh::keys::parse_public_key_base64` — ADR-004 decision 1) | no-panic on arbitrary key blobs (a non-blob is the parse's typed refusal); every accepted key's fingerprint is exactly the `SHA256:` + unpadded-standard-base64 contract shape (43-char body, canonical alphabet, decodes to a 32-byte digest, re-encodes canonically); determinism; the committed `ssh-keygen -lf` parity vectors still agree (the corpus-gate test row) |
|
| `fingerprint` | the crate's single fingerprint path ([`alkssh::fingerprint`] over `russh::keys::parse_public_key_base64`, ADR-004 decision 1) | no-panic on arbitrary key blobs (a non-blob is the parse's typed refusal); every accepted key's fingerprint is exactly the `SHA256:` + unpadded-standard-base64 contract shape (43-char body, canonical alphabet, decodes to a 32-byte digest, re-encodes canonically); determinism; the committed `ssh-keygen -lf` parity vectors still agree (the corpus-gate test row) |
|
||||||
| `key_decode` | the decrypt-at-use chain's decode arm (`decode_key_arm`, `src/key_source.rs` — the production `decrypt_and_decode` conversion exposed `#[doc(hidden)]`) + the underlying `decode_secret_key` passphrase arm (robustness drive — the production chain always passes `None`) | no-panic on arbitrary plaintext shapes (the vault's `String` shape, lossy from arbitrary bytes) and passphrase shapes; every failure on the production arm is exactly the typed `KeySourceError::Decode` class — never a catch-all, never a panic; the failure text never carries the plaintext; the `Zeroizing` posture rides the drive (the wrap is what the adapter seam applies); determinism |
|
| `key_decode` | the decrypt-at-use chain's decode arm (`decode_key_arm`, `src/key_source.rs`, the production `decrypt_and_decode` conversion exposed `#[doc(hidden)]`) + the underlying `decode_secret_key` passphrase arm (a robustness drive; the production chain always passes `None`) | no-panic on arbitrary plaintext shapes (the vault's `String` shape, lossy from arbitrary bytes) and passphrase shapes; every failure on the production arm is exactly the typed `KeySourceError::Decode` class, never a catch-all, never a panic; the failure text never carries the plaintext; the `Zeroizing` posture rides the drive (the wrap is what the adapter seam applies); determinism |
|
||||||
| `terminal_params` | the `pty_request` args → `TerminalParamsWire` conversion (`terminal_params`, `src/tty_map.rs` — OQ-44's SSH modes → reserved-modes `{}` JSON) | no-panic on arbitrary term strings, u32 dimensions, and mode blobs (the modes are never parsed — reserved); the dimension clamp is exactly the recorded cap (`min(value, 65535)`, never a magnitude truncation); `term` preserved verbatim; the reserved-modes JSON is always exactly `{}`; the full serialized wire shape is deterministic per input |
|
| `terminal_params` | the `pty_request` args → `TerminalParamsWire` conversion (`terminal_params`, `src/tty_map.rs`, OQ-44's SSH modes → reserved-modes `{}` JSON) | no-panic on arbitrary term strings, u32 dimensions, and mode blobs (the modes are never parsed; they are reserved); the dimension clamp is exactly the recorded cap (`min(value, 65535)`, never a magnitude truncation); `term` preserved verbatim; the reserved-modes JSON is always exactly `{}`; the full serialized wire shape is deterministic per input |
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
`cargo fuzz build` and `cargo fuzz run` must be executed with `fuzz/`
|
`cargo fuzz build` and `cargo fuzz run` must be executed with `fuzz/`
|
||||||
(or deeper) as the working directory so rustup selects the pinned
|
(or deeper) as the working directory so rustup selects the pinned
|
||||||
nightly toolchain — the detached runner handles that itself.
|
nightly toolchain; the detached runner handles that itself.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# build (nightly, pinned by rust-toolchain.toml inside fuzz/; run from fuzz/)
|
# build (nightly, pinned by rust-toolchain.toml inside fuzz/; run from fuzz/)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
//! The exec-grammar target (`mapping.md` §Session-channel dispatch —
|
//! The exec-grammar target (`mapping.md` §Session-channel dispatch,
|
||||||
//! the git-parseable exec grammar + the dispatch parse path) — thin
|
//! the git-parseable exec grammar + the dispatch parse path): a thin
|
||||||
//! wrapper over the shared invariant battery.
|
//! wrapper over the shared invariant battery.
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
//! The fingerprint-format target (ADR-004 decision 1 — the `SHA256:` +
|
//! The fingerprint-format target (ADR-004 decision 1, the `SHA256:` +
|
||||||
//! unpadded base64 path) — thin wrapper over the shared invariant
|
//! unpadded base64 path): a thin wrapper over the shared invariant
|
||||||
//! battery.
|
//! battery.
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
//! The key-decode target (ADR-005 decision 2 — the decrypt-at-use
|
//! The key-decode target (ADR-005 decision 2, the decrypt-at-use
|
||||||
//! chain's `decode_secret_key` seam) — thin wrapper over the shared
|
//! chain's `decode_secret_key` seam): a thin wrapper over the shared
|
||||||
//! invariant battery.
|
//! invariant battery.
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
//! The terminal-parameters target (`tty_map.rs` — the `pty_request`
|
//! The terminal-parameters target (`tty_map.rs`, the `pty_request`
|
||||||
//! args → `TerminalParamsWire` conversion, the SSH modes → reserved
|
//! args → `TerminalParamsWire` conversion, the SSH modes → reserved
|
||||||
//! modes JSON) — thin wrapper over the shared invariant battery.
|
//! modes JSON): a thin wrapper over the shared invariant battery.
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! The corpus-replay plumbing: the committed seeds replay through the
|
//! The corpus-replay plumbing: the committed seeds replay through the
|
||||||
//! invariant batteries on the stable toolchain (`cargo test
|
//! invariant batteries on the stable toolchain (`cargo test
|
||||||
//! --manifest-path fuzz/shared/Cargo.toml` runs this in
|
//! --manifest-path fuzz/shared/Cargo.toml` runs this in
|
||||||
//! `#[cfg(test)]` modules across the ingress units — the quinn CI
|
//! `#[cfg(test)]` modules across the ingress units, the quinn CI
|
||||||
//! pattern), so the gate never needs a fuzzing session.
|
//! pattern), so the gate never needs a fuzzing session.
|
||||||
|
|
||||||
pub fn read_corpus(target: &str) -> Vec<Vec<u8>> {
|
pub fn read_corpus(target: &str) -> Vec<Vec<u8>> {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
//! The exec-grammar ingress (`mapping.md` §Session-channel dispatch,
|
//! The exec-grammar ingress (`mapping.md` §Session-channel dispatch,
|
||||||
//! `dispatch.rs`): the production `parse_exec` path driven on arbitrary
|
//! `dispatch.rs`): the production `parse_exec` path driven on arbitrary
|
||||||
//! command strings — no-panic, typed routing classes, and agreement
|
//! command strings: no-panic, typed routing classes, and agreement
|
||||||
//! with a from-spec re-derivation of the grammar. The production
|
//! with a from-spec re-derivation of the grammar. The production
|
||||||
//! ingress shape is the handler's: the per-length cap
|
//! ingress shape is the handler's: the per-length cap
|
||||||
//! (`Limits::max_exec_command_len`) is enforced *before* the parse
|
//! (`Limits::max_exec_command_len`) is enforced *before* the parse
|
||||||
//! (decline first, alloc never — the cap's pre-parse placement is
|
//! (decline first, alloc never; the cap's pre-parse placement is
|
||||||
//! pinned by the root's contract rows in `tests/dispatch_core.rs`);
|
//! pinned by the root's contract rows in `tests/dispatch_core.rs`);
|
||||||
//! everything behind the cap is this battery: a byte string →
|
//! everything behind the cap is this battery: a byte string →
|
||||||
//! `std::str::from_utf8(..).unwrap_or("")` → the parse (invalid UTF-8
|
//! `std::str::from_utf8(..).unwrap_or("")` → the parse (invalid UTF-8
|
||||||
@@ -43,7 +43,7 @@ pub fn fuzz_exec_parse(data: &[u8]) {
|
|||||||
|
|
||||||
/// The from-spec oracle: the grammar re-derived from the dispatch
|
/// The from-spec oracle: the grammar re-derived from the dispatch
|
||||||
/// module's documented rules, sharing no code with the
|
/// module's documented rules, sharing no code with the
|
||||||
/// implementation — the differential pair (the repo_id target's
|
/// implementation, the differential pair (the repo_id target's
|
||||||
/// oracle pattern).
|
/// oracle pattern).
|
||||||
fn spec_parse(command: &str) -> ExecRoute {
|
fn spec_parse(command: &str) -> ExecRoute {
|
||||||
let (token, argument) = match command.split_once(' ') {
|
let (token, argument) = match command.split_once(' ') {
|
||||||
@@ -64,7 +64,7 @@ fn spec_parse(command: &str) -> ExecRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The serving-service arm of the oracle: a serving token must carry
|
/// The serving-service arm of the oracle: a serving token must carry
|
||||||
/// a non-empty, path-shaped argument (SSH exec must carry the repo —
|
/// a non-empty, path-shaped argument (SSH exec must carry the repo;
|
||||||
/// service-only is the decline-with-reason class; anything else
|
/// service-only is the decline-with-reason class; anything else
|
||||||
/// unparseable is the pipe row).
|
/// unparseable is the pipe row).
|
||||||
fn spec_serving(service: GitServiceRoute, argument: Option<&str>) -> ExecRoute {
|
fn spec_serving(service: GitServiceRoute, argument: Option<&str>) -> ExecRoute {
|
||||||
@@ -78,9 +78,9 @@ fn spec_serving(service: GitServiceRoute, argument: Option<&str>) -> ExecRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The argument grammar re-derived: a path-shaped argument — fully
|
/// The argument grammar re-derived: a path-shaped argument, either
|
||||||
/// single-quote-wrapped (a non-empty inner ending in a quote) or
|
/// fully single-quote-wrapped (a non-empty inner ending in a quote) or
|
||||||
/// unquoted absolute — normalized by stripping the quotes, one
|
/// unquoted absolute, normalized by stripping the quotes, one
|
||||||
/// leading `/`, and one trailing `.git` suffix (an empty-after-strip
|
/// leading `/`, and one trailing `.git` suffix (an empty-after-strip
|
||||||
/// prefix keeps the suffix; the git row's validation declines the
|
/// prefix keeps the suffix; the git row's validation declines the
|
||||||
/// empties, never a silent emptying).
|
/// empties, never a silent emptying).
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! The fingerprint format ingress (ADR-004 decision 1,
|
//! The fingerprint format ingress (ADR-004 decision 1,
|
||||||
//! `fingerprint.rs`): arbitrary key blobs through the crate's single
|
//! `fingerprint.rs`): arbitrary key blobs through the crate's single
|
||||||
//! fingerprint path — the composition of russh's public OpenSSH blob
|
//! fingerprint path: the composition of russh's public OpenSSH blob
|
||||||
//! parse (`russh::keys::parse_public_key_base64`, the same re-export
|
//! parse (`russh::keys::parse_public_key_base64`, the same re-export
|
||||||
//! the crate's surface rides) and the crate's public
|
//! the crate's surface rides) and the crate's public
|
||||||
//! [`alkssh::fingerprint`]. The ingress is fully public API — the
|
//! [`alkssh::fingerprint`]. The ingress is fully public API; the
|
||||||
//! recorded exposure decision needs no `#[doc(hidden)]` surface.
|
//! recorded exposure decision needs no `#[doc(hidden)]` surface.
|
||||||
//!
|
//!
|
||||||
//! Invariants: no-panic on any byte string (a non-blob is the parse's
|
//! Invariants: no-panic on any byte string (a non-blob is the parse's
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
//! accepted key's fingerprint is exactly the `SHA256:` + unpadded
|
//! accepted key's fingerprint is exactly the `SHA256:` + unpadded
|
||||||
//! standard base64 contract shape (one `:` separator, 43-char body,
|
//! standard base64 contract shape (one `:` separator, 43-char body,
|
||||||
//! canonical alphabet, 32-byte digest decoded exactly); determinism
|
//! canonical alphabet, 32-byte digest decoded exactly); determinism
|
||||||
//! (same key, same string — and the same string re-parses to the same
|
//! (same key, same string; the same string re-parses to the same
|
||||||
//! verdict). The committed `ssh-keygen -lf` parity vectors
|
//! verdict). The committed `ssh-keygen -lf` parity vectors
|
||||||
//! (`tests/vectors/`) are replayed by the corpus-gate tests below.
|
//! (`tests/vectors/`) are replayed by the corpus-gate tests below.
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ fn decode_unpadded(body: &str) -> Option<Vec<u8>> {
|
|||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A from-spec unpadded standard-base64 encoder — the re-encode half.
|
/// A from-spec unpadded standard-base64 encoder: the re-encode half.
|
||||||
fn encode_unpadded(bytes: &[u8]) -> String {
|
fn encode_unpadded(bytes: &[u8]) -> String {
|
||||||
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
//! The key-decode ingress (ADR-005 decision 2, `key_source.rs`): the
|
//! The key-decode ingress (ADR-005 decision 2, `key_source.rs`): the
|
||||||
//! decrypt-at-use chain's one-time `decode_secret_key` conversion —
|
//! decrypt-at-use chain's one-time `decode_secret_key` conversion,
|
||||||
//! driven through the production arm itself
|
//! driven through the production arm itself
|
||||||
//! ([`alkssh::decode_key_arm`], the `#[doc(hidden)]` exposure that is
|
//! ([`alkssh::decode_key_arm`], the `#[doc(hidden)]` exposure that is
|
||||||
//! `decrypt_and_decode`'s exact decode stage, not a fork).
|
//! `decrypt_and_decode`'s exact decode stage, not a fork).
|
||||||
//!
|
//!
|
||||||
//! Invariants: no-panic on arbitrary plaintext shapes (the production
|
//! Invariants: no-panic on arbitrary plaintext shapes (the production
|
||||||
//! plaintext is the vault's `String` — valid UTF-8 by return type; the
|
//! plaintext is the vault's `Zeroizing<String>`, valid UTF-8 by
|
||||||
//! battery shapes arbitrary bytes into that form via the lossy
|
//! return type; the battery shapes arbitrary bytes into that form via
|
||||||
//! conversion and drives it through the same `Zeroizing` wrap the
|
//! the lossy conversion and drives it through the same zeroized
|
||||||
//! adapter seam applies) and arbitrary passphrase shapes (the
|
//! wrap the vault's `decrypt` return applies, alkssh 0.2.0 seam) and
|
||||||
//! underlying decode's passphrase arm — the production chain always
|
//! arbitrary passphrase shapes (the
|
||||||
|
//! underlying decode's passphrase arm; the production chain always
|
||||||
//! passes `None`; the `Some` arm is robustness drive only); every
|
//! passes `None`; the `Some` arm is robustness drive only); every
|
||||||
//! failure is the typed [`KeySourceError::Decode`] class — never a
|
//! failure is the typed [`KeySourceError::Decode`] class, never a
|
||||||
//! catch-all, never another variant on this arm, never a panic; the
|
//! catch-all, never another variant on this arm, never a panic; the
|
||||||
//! failure text never carries the plaintext (the assertable half of
|
//! failure text never carries the plaintext (the assertable half of
|
||||||
//! the Zeroizing posture — the plaintext exists solely inside the
|
//! the Zeroizing posture: the plaintext exists solely inside the
|
||||||
//! call, dropped zeroized; no retention, no static cache — and the
|
//! call, dropped zeroized, with no retention and no static cache; the
|
||||||
//! typed error's only payload is the decode machinery's message).
|
//! typed error's only payload is the decode machinery's message
|
||||||
|
//! class, never the plaintext).
|
||||||
//!
|
//!
|
||||||
//! The no-echo invariant's assertable form is *the bounded-message
|
//! The no-echo invariant's assertable form is *the bounded-message
|
||||||
//! pair* (reshaped in-task after the first campaign's finding): the
|
//! pair* (reshaped in-task after the first campaign's finding): the
|
||||||
//! failure text is a bounded fixed message (≤ 256 bytes — the decode
|
//! failure text is a bounded fixed message (≤ 256 bytes; the decode
|
||||||
//! machinery's vocabulary, never a growable echo), and no 256-byte
|
//! machinery's vocabulary, never a growable echo), and no 256-byte
|
||||||
//! contiguous plaintext run appears in it — which, given the bound,
|
//! contiguous plaintext run appears in it, which, given the bound,
|
||||||
//! cannot false-fire (a string shorter than 256 bytes cannot contain a
|
//! cannot false-fire (a string shorter than 256 bytes cannot contain a
|
||||||
//! 256-byte run). A plaintext-`contains`-message check in the other
|
//! 256-byte run). A plaintext-`contains`-message check in the other
|
||||||
//! direction was unsound: an attacker-chosen plaintext can be a
|
//! direction was unsound: an attacker-chosen plaintext can be a
|
||||||
@@ -68,7 +70,7 @@ fn fingerprint(key: &russh::keys::PrivateKey) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The typed-class invariant: `Ok` or exactly
|
/// The typed-class invariant: `Ok` or exactly
|
||||||
/// [`KeySourceError::Decode`] — the decode arm's only failure shape —
|
/// [`KeySourceError::Decode`] (the decode arm's only failure shape),
|
||||||
/// and the Decode text is the bounded fixed-message pair (see the
|
/// and the Decode text is the bounded fixed-message pair (see the
|
||||||
/// module docs; the two assertions are jointly airtight).
|
/// module docs; the two assertions are jointly airtight).
|
||||||
fn typed_decode_class(outcome: &Result<russh::keys::PrivateKey, KeySourceError>, plaintext: &str) {
|
fn typed_decode_class(outcome: &Result<russh::keys::PrivateKey, KeySourceError>, plaintext: &str) {
|
||||||
@@ -121,7 +123,7 @@ mod invariants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A plaintext that is not OpenSSH-PEM key material is a
|
/// A plaintext that is not OpenSSH-PEM key material is a
|
||||||
/// [`KeySourceError::Decode`] refusal (the inner-format contract —
|
/// [`KeySourceError::Decode`] refusal (the inner-format contract:
|
||||||
/// decision 1, never a fallback).
|
/// decision 1, never a fallback).
|
||||||
#[test]
|
#[test]
|
||||||
fn non_key_plaintext_is_the_typed_decode_refusal() {
|
fn non_key_plaintext_is_the_typed_decode_refusal() {
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
//! The terminal-parameters ingress (`tty_map.rs`, OQ-44's reserved
|
//! The terminal-parameters ingress (`tty_map.rs`, OQ-44's reserved
|
||||||
//! modes): the `pty_request` args → `TerminalParamsWire` conversion
|
//! modes): the `pty_request` args → `TerminalParamsWire` conversion
|
||||||
//! driven through the production [terminal_params] surface (the
|
//! driven through the production [terminal_params] surface (the
|
||||||
//! `#[doc(hidden)]` exposure — `handler.rs`'s `pty_request` row
|
//! `#[doc(hidden)]` exposure: `handler.rs`'s `pty_request` row
|
||||||
//! constructs the stored params with exactly this function).
|
//! constructs the stored params with exactly this function).
|
||||||
//!
|
//!
|
||||||
//! The harness's framed feed (deterministic, seed-friendly):
|
//! The harness's framed feed (deterministic, seed-friendly):
|
||||||
//! `term_len` (u32 LE, bounded to the remaining input) + the term
|
//! `term_len` (u32 LE, bounded to the remaining input) + the term
|
||||||
//! bytes + four u32 LE dimensions (`cols`/`rows`/pixel width/height,
|
//! bytes + four u32 LE dimensions (`cols`/`rows`/pixel width/height,
|
||||||
//! zero-defaulted when the input runs out) + an arbitrary trailing
|
//! zero-defaulted when the input runs out) + an arbitrary trailing
|
||||||
//! mode blob. The mode blob is *never parsed* — the SSH ttymodes ride
|
//! mode blob. The mode blob is *never parsed*; the SSH ttymodes ride
|
||||||
//! the reserved `{}` JSON (OQ-44, backends ignore modes in v1) — so
|
//! the reserved `{}` JSON (OQ-44, backends ignore modes in v1), so
|
||||||
//! the battery's totality over arbitrary mode blobs is by
|
//! the battery's totality over arbitrary mode blobs is by
|
||||||
//! construction; the framing demonstrates it (the conversion's
|
//! construction; the framing demonstrates it (the conversion's
|
||||||
//! outcome never varies with the blob).
|
//! outcome never varies with the blob).
|
||||||
//!
|
//!
|
||||||
//! Invariants: no-panic on any term string (empty, unicode, control
|
//! Invariants: no-panic on any term string (empty, unicode, control
|
||||||
//! characters) and any u32 dimensions; the dimension clamp is the
|
//! characters) and any u32 dimensions; the dimension clamp is the
|
||||||
//! recorded cap (`min(value, 65535)` — a dimension beyond 65535 is
|
//! recorded cap (`min(value, 65535)`; a dimension beyond 65535 is
|
||||||
//! nonsense on both papers) computed exactly, never a magnitude
|
//! nonsense on both papers) computed exactly, never a magnitude
|
||||||
//! truncation; `term` is preserved verbatim; the reserved-modes JSON
|
//! truncation; `term` is preserved verbatim; the reserved-modes JSON
|
||||||
//! is always exactly the empty object; the full wire shape is
|
//! is always exactly the empty object; the full wire shape is
|
||||||
@@ -89,7 +89,7 @@ pub fn fuzz_terminal_params(data: &[u8]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The cursor's u32 (zero-defaulted when the framed field is absent —
|
/// The cursor's u32 (zero-defaulted when the framed field is absent;
|
||||||
/// the feed is total over any input).
|
/// the feed is total over any input).
|
||||||
fn take_u32(cursor: &mut &[u8]) -> u32 {
|
fn take_u32(cursor: &mut &[u8]) -> u32 {
|
||||||
if cursor.len() >= 4 {
|
if cursor.len() >= 4 {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# The decline-matrix runner (tasks/implementation/w5-decline-matrix.md):
|
# The decline-matrix runner (tasks/implementation/w5-decline-matrix.md):
|
||||||
# the per-build verification set as one command — `cargo test` per
|
# the per-build verification set as one command: `cargo test` per
|
||||||
# feature build (none / git / session / tunnel / sftp-only /
|
# feature build (none / git / session / tunnel / sftp-only /
|
||||||
# --all-features; the sftp-only run is the no-op-feature fact itself
|
# --all-features; the sftp-only run is the no-op-feature fact itself
|
||||||
# verified), clippy ×2 (-D warnings, with and without
|
# verified), clippy ×2 (-D warnings, with and without
|
||||||
|
|||||||
+19
-18
@@ -1,8 +1,8 @@
|
|||||||
//! The server-direction mount point (ADR-001) — [`SshAdapter`], the
|
//! The server-direction mount point (ADR-001): [`SshAdapter`], the
|
||||||
//! constructed russh server stack every mapping composes into.
|
//! constructed russh server stack every mapping composes into.
|
||||||
//!
|
//!
|
||||||
//! Construction ([`SshAdapter::new`], ADR-005 decision 6): the
|
//! Construction ([`SshAdapter::new`], ADR-005 decision 6): the
|
||||||
//! injected [`HostKeySource`] loads **decrypt-at-construction** — a
|
//! injected [`HostKeySource`] loads **decrypt-at-construction**; a
|
||||||
//! locked / absent / decryption-failed / unknown-version / undecodable
|
//! locked / absent / decryption-failed / unknown-version / undecodable
|
||||||
//! load means **no server is constructed**: the error surfaces as
|
//! load means **no server is constructed**: the error surfaces as
|
||||||
//! [`AdapterError::Construction`] with the original [`KeySourceError`]
|
//! [`AdapterError::Construction`] with the original [`KeySourceError`]
|
||||||
@@ -11,34 +11,35 @@
|
|||||||
//! host-key source contract's [`KeySourceError::Absent`] shape).
|
//! host-key source contract's [`KeySourceError::Absent`] shape).
|
||||||
//!
|
//!
|
||||||
//! The russh server `Config` (ADR-002 surface notes; ADR-004 decision
|
//! The russh server `Config` (ADR-002 surface notes; ADR-004 decision
|
||||||
//! 4): `methods` is `{publickey}` **exactly** (honest advertisement —
|
//! 4): `methods` is `{publickey}` **exactly** (honest advertisement:
|
||||||
//! `MethodSet::from(&[MethodKind::PublicKey][..])`, never russh's
|
//! `MethodSet::from(&[MethodKind::PublicKey][..])`, never russh's
|
||||||
//! `server_supported()`), the loaded host keys ride `Config::keys`,
|
//! `server_supported()`), the loaded host keys ride `Config::keys`,
|
||||||
//! and the [`Limits`] knobs map one-for-one:
|
//! and the [`Limits`] knobs map one-for-one:
|
||||||
//! `window_size` / `maximum_packet_size` / `channel_buffer_size` /
|
//! `window_size` / `maximum_packet_size` / `channel_buffer_size` /
|
||||||
//! `max_auth_attempts` verbatim, `inactivity_timeout` as **always
|
//! `max_auth_attempts` verbatim, `inactivity_timeout` as **always
|
||||||
//! `Some`** — the bounded-resources posture (the crate never
|
//! `Some`**, the bounded-resources posture (the crate never
|
||||||
//! constructs a session with no wall clock). Everything else stays at
|
//! constructs a session with no wall clock). Everything else stays at
|
||||||
//! russh 0.65's own default posture (`Config::default()`, left
|
//! russh 0.65's own default posture (`Config::default()`, left
|
||||||
//! un-touched deliberately): the rekey `Limits` stay russh-enforced at
|
//! un-touched deliberately): the rekey `Limits` stay russh-enforced at
|
||||||
//! their RFC-4253 §9 defaults, `auth_rejection_time` stays russh's
|
//! their RFC-4253 §9 defaults, `auth_rejection_time` stays russh's
|
||||||
//! constant-rejection knob, and the preferred-algorithm set stays
|
//! constant-rejection knob, and the preferred-algorithm set stays
|
||||||
//! russh's — the ed25519-only key-algorithm posture holds by the
|
//! russh's; the ed25519-only key-algorithm posture holds by the
|
||||||
//! loaded key set itself (ADR-002 decision 4) and by the auth bridge's
|
//! loaded key set itself (ADR-002 decision 4) and by the auth bridge's
|
||||||
//! explicit non-ed25519 decline (`w2-auth-bridge`).
|
//! explicit non-ed25519 decline (`w2-auth-bridge`).
|
||||||
//!
|
//!
|
||||||
//! The mount ([`SshAdapter::handle`], ADR-001's contract): a generic
|
//! The mount ([`SshAdapter::handle`], ADR-001's contract): a generic
|
||||||
//! byte-duplex stream in, the full russh server stack per stream via
|
//! byte-duplex stream in, the full russh server stack per stream via
|
||||||
//! [`russh::server::run_stream`], returned **run to completion** —
|
//! [`russh::server::run_stream`], returned **run to completion**;
|
||||||
//! the edge-termination shape only, no ALPN code, no relay leg (the
|
//! the edge-termination shape only, no ALPN code, no relay leg (the
|
||||||
//! `alk/ssh` pin is a ledger entry; nothing here speaks it). The
|
//! `alk/ssh` pin is a ledger entry; nothing here speaks it). The
|
||||||
//! in-flight framing is the alkhttp door precedent: `handle` is an
|
//! in-flight framing is the alkhttp door precedent: `handle` is an
|
||||||
//! async per-stream call whose completion is the session's end, so the
|
//! async per-stream call whose completion is the session's end, so the
|
||||||
//! assembly's accept loop mounts per stream via `tokio::spawn` — the
|
//! assembly's accept loop mounts per stream via `tokio::spawn`, and
|
||||||
//! serving loop itself stays spawned inside russh (ADR-006 decision 4,
|
//! the serving loop itself stays spawned inside russh (ADR-006
|
||||||
//! which `run_stream` already satisfies: the session's packet loop
|
//! decision 4, which `run_stream` already satisfies: the session's
|
||||||
//! leaves on its own task), and the mount's return type stays
|
//! packet loop leaves on its own task), and the mount's return type
|
||||||
//! `Result<(), AdapterError>` — no russh type rides the signature.
|
//! stays `Result<(), AdapterError>` (no russh type rides the
|
||||||
|
//! signature).
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ use crate::handler::SessionHandler;
|
|||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
use crate::tunnel_map;
|
use crate::tunnel_map;
|
||||||
|
|
||||||
/// The constructed server stack — the substrate every channel-type
|
/// The constructed server stack, the substrate every channel-type
|
||||||
/// mapping composes into (ADR-001's `SshAdapter`).
|
/// mapping composes into (ADR-001's `SshAdapter`).
|
||||||
///
|
///
|
||||||
/// Built by [`SshAdapter::new`] from the assembly-supplied
|
/// Built by [`SshAdapter::new`] from the assembly-supplied
|
||||||
@@ -67,8 +68,8 @@ pub struct SshAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SshAdapter {
|
impl SshAdapter {
|
||||||
/// Constructs the adapter — decrypt-at-construction of the host
|
/// Constructs the adapter: decrypt-at-construction of the host
|
||||||
/// key set (ADR-005 decision 6): a failed or empty load is no
|
/// key set (ADR-005 decision 6); a failed or empty load is no
|
||||||
/// server, with the [`KeySourceError`] refused through
|
/// server, with the [`KeySourceError`] refused through
|
||||||
/// [`AdapterError::Construction`].
|
/// [`AdapterError::Construction`].
|
||||||
pub async fn new(serve: ServeConfig) -> Result<Self, AdapterError> {
|
pub async fn new(serve: ServeConfig) -> Result<Self, AdapterError> {
|
||||||
@@ -98,7 +99,7 @@ impl SshAdapter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The mounted russh server `Config` — in-crate inspection for the
|
/// The mounted russh server `Config`, in-crate inspection for the
|
||||||
/// contract tests (advertisement and knob mapping); not public
|
/// contract tests (advertisement and knob mapping); not public
|
||||||
/// surface: the mount's behavior is observable on the wire, never
|
/// surface: the mount's behavior is observable on the wire, never
|
||||||
/// through a russh type leak.
|
/// through a russh type leak.
|
||||||
@@ -108,7 +109,7 @@ impl SshAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The mount (ADR-001): mounts the full russh server stack on one
|
/// The mount (ADR-001): mounts the full russh server stack on one
|
||||||
/// generic duplex byte stream and runs it to completion — an
|
/// generic duplex byte stream and runs it to completion; an
|
||||||
/// accepted vanilla-SSH transport is authenticated and served on
|
/// accepted vanilla-SSH transport is authenticated and served on
|
||||||
/// this call's task; the assembly's accept loop mounts per stream
|
/// this call's task; the assembly's accept loop mounts per stream
|
||||||
/// with `tokio::spawn(adapter.handle(stream))`.
|
/// with `tokio::spawn(adapter.handle(stream))`.
|
||||||
@@ -126,7 +127,7 @@ impl SshAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The adapter's own `Config` construction from the serve-config — the
|
/// The adapter's own `Config` construction from the serve-config: the
|
||||||
/// documented subset mapped one-for-one, everything else at russh's
|
/// documented subset mapped one-for-one, everything else at russh's
|
||||||
/// default posture (see the module docs; ADR-002 surface notes).
|
/// default posture (see the module docs; ADR-002 surface notes).
|
||||||
fn build_server_config(serve: &ServeConfig, keys: Vec<PrivateKey>) -> russh::server::Config {
|
fn build_server_config(serve: &ServeConfig, keys: Vec<PrivateKey>) -> russh::server::Config {
|
||||||
@@ -160,7 +161,7 @@ mod tests {
|
|||||||
const NOOP: alkcall::core::auth::NoopIdentityProvider =
|
const NOOP: alkcall::core::auth::NoopIdentityProvider =
|
||||||
alkcall::core::auth::NoopIdentityProvider;
|
alkcall::core::auth::NoopIdentityProvider;
|
||||||
|
|
||||||
/// The committed test-key fixtures — the wave-1 fixture set, read
|
/// The committed test-key fixtures (the wave-1 fixture set), read
|
||||||
/// through `include_str!` per the wave-1 convention (the
|
/// through `include_str!` per the wave-1 convention (the
|
||||||
/// `tests/common` double module is the tests/-side owner; src-unit
|
/// `tests/common` double module is the tests/-side owner; src-unit
|
||||||
/// tests read the fixture files, never its exports).
|
/// tests read the fixture files, never its exports).
|
||||||
|
|||||||
+24
-24
@@ -1,9 +1,9 @@
|
|||||||
//! The in-crate concurrent-channel cap (`w2-bounded-resources`) — the
|
//! The in-crate concurrent-channel cap (`w2-bounded-resources`): the
|
||||||
//! bound itself, carried by the per-connection handler
|
//! bound itself, carried by the per-connection handler
|
||||||
//! ([`crate::handler`]) and enforced at every `channel_open_*` accept
|
//! ([`crate::handler`]) and enforced at every `channel_open_*` accept
|
||||||
//! path (`overview.md` §Bounded-resources policy: "concurrent channels
|
//! path (`overview.md` §Bounded-resources policy: "concurrent channels
|
||||||
//! per session — ours (in-crate) + per-identity policy when bridged";
|
//! per session (ours, in-crate) + per-identity policy when bridged";
|
||||||
//! AGENTS.md convention 9 — SSH servers are internet-facing; unbounded
|
//! AGENTS.md convention 9: SSH servers are internet-facing; unbounded
|
||||||
//! loops and unbounded buffers are bugs).
|
//! loops and unbounded buffers are bugs).
|
||||||
//!
|
//!
|
||||||
//! ## Shape
|
//! ## Shape
|
||||||
@@ -11,12 +11,12 @@
|
|||||||
//! [`ChannelCap`] is a per-session count of active channels. Admission
|
//! [`ChannelCap`] is a per-session count of active channels. Admission
|
||||||
//! ([`ChannelCap::admit`]) is the check-and-increment the accept paths
|
//! ([`ChannelCap::admit`]) is the check-and-increment the accept paths
|
||||||
//! call: room under the cap admits (the count rises), otherwise the
|
//! call: room under the cap admits (the count rises), otherwise the
|
||||||
//! open is refused — a clean decline, **never queued, never half-open**
|
//! open is refused: a clean decline, **never queued, never half-open**
|
||||||
//! (the fail-closed posture; the refusal reason is
|
//! (the fail-closed posture; the refusal reason is
|
||||||
//! [`crate::dispatch::cap_exceeded_open_decline`]). Release
|
//! [`crate::dispatch::cap_exceeded_open_decline`]). Release
|
||||||
//! ([`ChannelCap::release`]) fires from the `channel_close` hook for
|
//! ([`ChannelCap::release`]) fires from the `channel_close` hook for
|
||||||
//! established channels — and, since `w3-tunnel-direct`, from a
|
//! established channels, and, since `w3-tunnel-direct`, from a
|
||||||
//! spawned route-1 bridge task on a fabric-open failure — and, since
|
//! spawned route-1 bridge task on a fabric-open failure, and, since
|
||||||
//! `w3-tunnel-listen`, from the spawned `-R` arrival pumps on the
|
//! `w3-tunnel-listen`, from the spawned `-R` arrival pumps on the
|
||||||
//! never-established paths (client-rejected open, dead session): see
|
//! never-established paths (client-rejected open, dead session): see
|
||||||
//! the release-sites section.
|
//! the release-sites section.
|
||||||
@@ -27,36 +27,36 @@
|
|||||||
//! Every admission has exactly one release, and the paths are
|
//! Every admission has exactly one release, and the paths are
|
||||||
//! disjoint by the wire shape:
|
//! disjoint by the wire shape:
|
||||||
//!
|
//!
|
||||||
//! 1. **The established-channel close** — the `channel_close` hook.
|
//! 1. **The established-channel close**: the `channel_close` hook.
|
||||||
//! russh routes `CHANNEL_CLOSE` only for channels it considers
|
//! russh routes `CHANNEL_CLOSE` only for channels it considers
|
||||||
//! established; a declined open never established one, and a close
|
//! established; a declined open never established one, and a close
|
||||||
//! is consumed at most once (russh removes the channel before the
|
//! is consumed at most once (russh removes the channel before the
|
||||||
//! hook).
|
//! hook).
|
||||||
//! 2. **The admitted-but-rejected bridge** — the route-1 bridge's
|
//! 2. **The admitted-but-rejected bridge**: the route-1 bridge's
|
||||||
//! failure path (`w3-tunnel-direct`). The open hook admits and
|
//! failure path (`w3-tunnel-direct`). The open hook admits and
|
||||||
//! holds the count across the fabric open; on fabric failure the
|
//! holds the count across the fabric open; on fabric failure the
|
||||||
//! open is rejected with the explicit `ConnectFailed`
|
//! open is rejected with the explicit `ConnectFailed`
|
||||||
//! (ADR-006 decision 1: accept only after the fabric open
|
//! (ADR-006 decision 1: accept only after the fabric open
|
||||||
//! succeeds), the channel never establishes, and *no
|
//! succeeds), the channel never establishes, and *no
|
||||||
//! `channel_close` can ever fire* — so the count must be released
|
//! `channel_close` can ever fire*, so the count must be released
|
||||||
//! from where the failure is known: inside the spawned bridge
|
//! from where the failure is known: inside the spawned bridge
|
||||||
//! task, **before** the reject is answered (the very next open
|
//! task, **before** the reject is answered (the very next open
|
||||||
//! then observes the recovered budget — pinned by wire test). A
|
//! then observes the recovered budget; pinned by wire test). A
|
||||||
//! success never releases there (its close rides path 1).
|
//! success never releases there (its close rides path 1).
|
||||||
//! 3. **The `-R` arrival pumps** (`w3-tunnel-listen`): a fabric
|
//! 3. **The `-R` arrival pumps** (`w3-tunnel-listen`): a fabric
|
||||||
//! arrival is admitted into the owning route's count and opens the
|
//! arrival is admitted into the owning route's count and opens the
|
||||||
//! SSH `forwarded-tcpip`/`forwarded-streamlocal` channel toward
|
//! SSH `forwarded-tcpip`/`forwarded-streamlocal` channel toward
|
||||||
//! the vanilla client through the session handle. A rejected open —
|
//! the vanilla client through the session handle. A rejected open
|
||||||
//! the client's own refusal, or a dead session's send failure —
|
//! (the client's own refusal, or a dead session's send failure)
|
||||||
//! never establishes the channel, so the count releases inside the
|
//! never establishes the channel, so the count releases inside the
|
||||||
//! arrival task exactly once (an established channel's release is
|
//! arrival task exactly once (an established channel's release is
|
||||||
//! path 1's). These are the mirror of site 2 on the server-initiated
|
//! path 1's). These are the mirror of site 2 on the server-initiated
|
||||||
//! open direction — the acceptor vocabulary's cap rows.
|
//! open direction, the acceptor vocabulary's cap rows.
|
||||||
//!
|
//!
|
||||||
//! ## Why the mutex (the single-writer reasoning, updated twice)
|
//! ## Why the mutex (the single-writer reasoning, updated twice)
|
||||||
//!
|
//!
|
||||||
//! The wave-2 shape was a bare `usize` field written only by the
|
//! The wave-2 shape was a bare `usize` field written only by the
|
||||||
//! session's packet loop — sound because every hook runs inline on
|
//! session's packet loop, sound because every hook runs inline on
|
||||||
//! that loop with exclusive `&mut self` access. The bridge release
|
//! that loop with exclusive `&mut self` access. The bridge release
|
||||||
//! (`w3-tunnel-direct`) and the arrival admission (`w3-tunnel-listen`)
|
//! (`w3-tunnel-direct`) and the arrival admission (`w3-tunnel-listen`)
|
||||||
//! both break the single-writer property: those tasks are alkcall
|
//! both break the single-writer property: those tasks are alkcall
|
||||||
@@ -66,12 +66,12 @@
|
|||||||
//! critical sections are single integer ops (no await inside the
|
//! critical sections are single integer ops (no await inside the
|
||||||
//! lock), and all sites saturating. Channels never closed before
|
//! lock), and all sites saturating. Channels never closed before
|
||||||
//! session teardown stay counted until the handler dies with the
|
//! session teardown stay counted until the handler dies with the
|
||||||
//! session — per-session state, gone with the connection.
|
//! session (per-session state, gone with the connection).
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
/// The per-session active-channel count for the in-crate
|
/// The per-session active-channel count for the in-crate
|
||||||
/// concurrent-channel cap — see the module docs for the shape, the
|
/// concurrent-channel cap; see the module docs for the shape, the
|
||||||
/// release sites, and the writer/soundness reasoning. Shared with the
|
/// release sites, and the writer/soundness reasoning. Shared with the
|
||||||
/// spawned route-1 bridge tasks ([`Arc<ChannelCap>`]) exactly so the
|
/// spawned route-1 bridge tasks ([`Arc<ChannelCap>`]) exactly so the
|
||||||
/// fabric-failure release can fire from the task that knows it.
|
/// fabric-failure release can fire from the task that knows it.
|
||||||
@@ -82,13 +82,13 @@ pub(crate) struct ChannelCap {
|
|||||||
|
|
||||||
impl ChannelCap {
|
impl ChannelCap {
|
||||||
/// The accept-path admission decision: room under `cap` increments
|
/// The accept-path admission decision: room under `cap` increments
|
||||||
/// the count and admits; otherwise refuses (fail-closed — the
|
/// the count and admits; otherwise refuses (fail-closed: the
|
||||||
/// caller answers the open with a clean decline). `cap == 0`
|
/// caller answers the open with a clean decline). `cap == 0`
|
||||||
/// refuses every open. Call sites: the packet loop's open hooks
|
/// refuses every open. Call sites: the packet loop's open hooks
|
||||||
/// `w2`-style, and — since `w3-tunnel-listen` — the `-R` arrival
|
/// `w2`-style, and, since `w3-tunnel-listen`, the `-R` arrival
|
||||||
/// tasks (the door's forwarded-arrival admission: an arrival is
|
/// tasks (the door's forwarded-arrival admission: an arrival is
|
||||||
/// admitted on the owning route's count inside the spawned
|
/// admitted on the owning route's count inside the spawned
|
||||||
/// arrival pump — see the module doc's updated single-writer
|
/// arrival pump; see the module doc's updated single-writer
|
||||||
/// reasoning). The mutex makes both sound.
|
/// reasoning). The mutex makes both sound.
|
||||||
pub(crate) fn admit(&self, cap: usize) -> bool {
|
pub(crate) fn admit(&self, cap: usize) -> bool {
|
||||||
let mut active = self.active.lock();
|
let mut active = self.active.lock();
|
||||||
@@ -110,7 +110,7 @@ impl ChannelCap {
|
|||||||
*active = active.saturating_sub(1);
|
*active = active.saturating_sub(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The active count — in-crate inspection for the unit rows.
|
/// The active count, in-crate inspection for the unit rows.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn active(&self) -> usize {
|
pub(crate) fn active(&self) -> usize {
|
||||||
*self.active.lock()
|
*self.active.lock()
|
||||||
@@ -136,7 +136,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the task's cap mechanics (a close recovers
|
/// - *Contract stamp:* the task's cap mechanics (a close recovers
|
||||||
/// the cap — the count decrements on close).
|
/// the cap; the count decrements on close).
|
||||||
#[test]
|
#[test]
|
||||||
fn release_recovers_the_cap() {
|
fn release_recovers_the_cap() {
|
||||||
let cap = ChannelCap::default();
|
let cap = ChannelCap::default();
|
||||||
@@ -150,7 +150,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the fail-closed cap posture (`cap == 0` is
|
/// - *Contract stamp:* the fail-closed cap posture (`cap == 0` is
|
||||||
/// a valid, assembly-tunable refusal of every open — never a
|
/// a valid, assembly-tunable refusal of every open, never a
|
||||||
/// bypass).
|
/// bypass).
|
||||||
#[test]
|
#[test]
|
||||||
fn zero_cap_refuses_every_open() {
|
fn zero_cap_refuses_every_open() {
|
||||||
@@ -177,9 +177,9 @@ mod tests {
|
|||||||
|
|
||||||
/// - *Contract stamp:* `w3-tunnel-direct`'s recorded release
|
/// - *Contract stamp:* `w3-tunnel-direct`'s recorded release
|
||||||
/// mechanism (the admitted-but-rejected bridge releases inside
|
/// mechanism (the admitted-but-rejected bridge releases inside
|
||||||
/// the spawned task): the cap is shared safely — admission from
|
/// the spawned task): the cap is shared safely (admission from
|
||||||
/// the packet-loop site, release from anywhere that holds the
|
/// the packet-loop site, release from anywhere that holds the
|
||||||
/// `Arc` — and disjoint releases never double-count.
|
/// `Arc`), and disjoint releases never double-count.
|
||||||
#[test]
|
#[test]
|
||||||
fn the_cap_is_shareable_between_the_loop_and_bridge_tasks() {
|
fn the_cap_is_shareable_between_the_loop_and_bridge_tasks() {
|
||||||
let cap = std::sync::Arc::new(ChannelCap::default());
|
let cap = std::sync::Arc::new(ChannelCap::default());
|
||||||
|
|||||||
+38
-38
@@ -1,4 +1,4 @@
|
|||||||
//! The dial surface (`w4-dial`) — the door *outward* (`client.md`
|
//! The dial surface (`w4-dial`), the door *outward* (`client.md`
|
||||||
//! §The dial surface), the client-direction mirror of
|
//! §The dial surface), the client-direction mirror of
|
||||||
//! [`crate::adapter`]'s mount: the russh client stack every wave-4 op
|
//! [`crate::adapter`]'s mount: the russh client stack every wave-4 op
|
||||||
//! row composes into.
|
//! row composes into.
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
//! whole authenticated-dial sequence, **fail-closed at every step**:
|
//! whole authenticated-dial sequence, **fail-closed at every step**:
|
||||||
//!
|
//!
|
||||||
//! 1. **The decrypt-at-dial key load** (ADR-005 decision 2): the
|
//! 1. **The decrypt-at-dial key load** (ADR-005 decision 2): the
|
||||||
//! injected [`KeySource::load`] runs *here and nowhere else* —
|
//! injected [`KeySource::load`] runs *here and nowhere else*:
|
||||||
//! locked / absent / decryption / unknown-version / decode are all
|
//! locked / absent / decryption / unknown-version / decode are all
|
||||||
//! **no-dial outcomes** ([`DialError::KeySource`], the variant
|
//! **no-dial outcomes** ([`DialError::KeySource`], the variant
|
||||||
//! preserved). The decoded key lives for this dial; the vault is
|
//! preserved). The decoded key lives for this dial; the vault is
|
||||||
@@ -15,18 +15,18 @@
|
|||||||
//! established connections streaming, POC-2 §2).
|
//! established connections streaming, POC-2 §2).
|
||||||
//! 2. **The bounded russh client `Config`** ([`russh_client_config`])
|
//! 2. **The bounded russh client `Config`** ([`russh_client_config`])
|
||||||
//! from the [`Limits`]: the keepalive 15 s / 3 posture
|
//! from the [`Limits`]: the keepalive 15 s / 3 posture
|
||||||
//! (`reverse-runner.md` lesson 3 — OpenSSH
|
//! (`reverse-runner.md` lesson 3, OpenSSH
|
||||||
//! `ServerAliveInterval=15`/`CountMax=3` parity), the wall clock
|
//! `ServerAliveInterval=15`/`CountMax=3` parity), the wall clock
|
||||||
//! **always `Some`** (`inactivity_timeout` — the crate never
|
//! **always `Some`** (`inactivity_timeout`, the crate never
|
||||||
//! constructs a session with no clock), and the window/packet/
|
//! constructs a session with no clock), and the window/packet/
|
||||||
//! channel-buffer knobs verbatim. Everything else stays russh's
|
//! channel-buffer knobs verbatim. Everything else stays russh's
|
||||||
//! own default posture (the ADR-002 surface notes, mirrored from
|
//! own default posture (the ADR-002 surface notes, mirrored from
|
||||||
//! [`crate::adapter`]'s server mapping: rekey `Limits`
|
//! [`crate::adapter`]'s server mapping: rekey `Limits`
|
||||||
//! russh-enforced, preferred algorithms russh's — the ed25519-only
|
//! russh-enforced, preferred algorithms russh's; the ed25519-only
|
||||||
//! posture holds by the loaded key set itself). No client-side
|
//! posture holds by the loaded key set itself). No client-side
|
||||||
//! auth-attempt knob exists at this pin and none is needed: the
|
//! auth-attempt knob exists at this pin and none is needed: the
|
||||||
//! family contract is exactly **one publickey attempt**.
|
//! family contract is exactly **one publickey attempt**.
|
||||||
//! 3. **The policy call at `check_server_key`** (OQ-05) — the
|
//! 3. **The policy call at `check_server_key`** (OQ-05), the
|
||||||
//! wave-1 [`HostKeyPolicy`](crate::HostKeyPolicy) seam consumed by
|
//! wave-1 [`HostKeyPolicy`](crate::HostKeyPolicy) seam consumed by
|
||||||
//! the handler ([`crate::client_handler`]): the crate default
|
//! the handler ([`crate::client_handler`]): the crate default
|
||||||
//! [`RejectAll`](crate::RejectAll) connects to nothing; a
|
//! [`RejectAll`](crate::RejectAll) connects to nothing; a
|
||||||
@@ -34,23 +34,23 @@
|
|||||||
//! Declines surface as [`DialError::HostKeyRejected`].
|
//! Declines surface as [`DialError::HostKeyRejected`].
|
||||||
//! 4. **Publickey-only authentication** (the family contract):
|
//! 4. **Publickey-only authentication** (the family contract):
|
||||||
//! exactly one `authenticate_publickey` attempt from the loaded
|
//! exactly one `authenticate_publickey` attempt from the loaded
|
||||||
//! key — no password, no keyboard-interactive, no cert, no retry,
|
//! key (no password, no keyboard-interactive, no cert, no retry,
|
||||||
//! no fallback method (russh's handle methods for those are never
|
//! no fallback method (russh's handle methods for those are never
|
||||||
//! exposed through [`SshConnection`]); a refusal is
|
//! exposed through [`SshConnection`])); a refusal is
|
||||||
//! [`DialError::AuthRejected`] and the attempt is closed cleanly.
|
//! [`DialError::AuthRejected`] and the attempt is closed cleanly.
|
||||||
//!
|
//!
|
||||||
//! **The connection handle** ([`SshConnection`]) is the type
|
//! **The connection handle** ([`SshConnection`]) is the type
|
||||||
//! everything else in the wave rides (the op rows open channels on
|
//! everything else in the wave rides (the op rows open channels on
|
||||||
//! it; the `-R` leg installs the acceptor registry over it) — the
|
//! it; the `-R` leg installs the acceptor registry over it), the
|
||||||
//! russh `client::Handle` wrapped with the crate's surface, the
|
//! russh `client::Handle` wrapped with the crate's surface, the
|
||||||
//! policy, limits and endpoint labels carried. The connection handle's
|
//! policy, limits and endpoint labels carried. The connection handle's
|
||||||
//! clean-close path is [`SshConnection::close`]; the listener handles'
|
//! clean-close path is [`SshConnection::close`]; the listener handles'
|
||||||
//! surface is [`crate::client_accept`]'s.
|
//! surface is [`crate::client_accept`]'s.
|
||||||
//!
|
//!
|
||||||
//! ## Lifecycle posture (`w4-lifecycle` — the OQ-12 resolution in code)
|
//! ## Lifecycle posture (`w4-lifecycle`, the OQ-12 resolution in code)
|
||||||
//!
|
//!
|
||||||
//! The handle surface is **primitives, not policy** (reverse-runner
|
//! The handle surface is **primitives, not policy** (reverse-runner
|
||||||
//! fact 4): reconnect-and-restore (rebinding persisted tunnel ports —
|
//! fact 4): reconnect-and-restore (rebinding persisted tunnel ports,
|
||||||
//! the alkminer flow) is *assembly* composition over these
|
//! the alkminer flow) is *assembly* composition over these
|
||||||
//! primitives; the crate provides bound ports, handles, cancellation,
|
//! primitives; the crate provides bound ports, handles, cancellation,
|
||||||
//! clean close, and never owns a reconnect/restore policy. Two
|
//! clean close, and never owns a reconnect/restore policy. Two
|
||||||
@@ -58,18 +58,18 @@
|
|||||||
//!
|
//!
|
||||||
//! - **Drop is never the clean path.** At this russh pin dropping the
|
//! - **Drop is never the clean path.** At this russh pin dropping the
|
||||||
//! last wire-stack handle ends the runner and the transport goes
|
//! last wire-stack handle ends the runner and the transport goes
|
||||||
//! down **abruptly** — the peer observes a broken session (its
|
//! down **abruptly**: the peer observes a broken session (its
|
||||||
//! serving outcome is an error wire class), never the clean
|
//! serving outcome is an error wire class), never the clean
|
||||||
//! disconnect message. So the surface never relies on drop timing
|
//! disconnect message. So the surface never relies on drop timing
|
||||||
//! for clean state: only [`SshConnection::close`] sends
|
//! for clean state: only [`SshConnection::close`] sends
|
||||||
//! `Disconnect::ByApplication` — the peer observes a clean
|
//! `Disconnect::ByApplication`, the peer observes a clean
|
||||||
//! disconnect, no half-open channels (row-pinned through the
|
//! disconnect, no half-open channels (row-pinned through the
|
||||||
//! server double, both directions of the boundary: abrupt-on-drop,
|
//! server double, both directions of the boundary: abrupt-on-drop,
|
||||||
//! clean-on-close).
|
//! clean-on-close).
|
||||||
//! And a cancellation is async — it cannot hide inside a sync
|
//! And a cancellation is async; it cannot hide inside a sync
|
||||||
//! `Drop` hook, so no handle hides a `Drop` side effect: the
|
//! `Drop` hook, so no handle hides a `Drop` side effect: the
|
||||||
//! listener handles' drop frees *nothing* on their wire state.
|
//! listener handles' drop frees *nothing* on their wire state.
|
||||||
//! - **Every listener/handle is explicitly cancellable** — the `-R`
|
//! - **Every listener/handle is explicitly cancellable**: the `-R`
|
||||||
//! listens return the [`ReverseListener`](crate::client_accept::
|
//! listens return the [`ReverseListener`](crate::client_accept::
|
||||||
//! ReverseListener) handles (the actually-bound endpoint carried;
|
//! ReverseListener) handles (the actually-bound endpoint carried;
|
||||||
//! `cancel()` the wire-honest teardown), the connection handle
|
//! `cancel()` the wire-honest teardown), the connection handle
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
//! [`russh_client_config`] (the bounded-knob mapping, asserted
|
//! [`russh_client_config`] (the bounded-knob mapping, asserted
|
||||||
//! verbatim) and [`ClientHandler`](crate::client_handler::ClientHandler)
|
//! verbatim) and [`ClientHandler`](crate::client_handler::ClientHandler)
|
||||||
//! (the handler skeleton; `client_handler.rs`). The wire vehicle is
|
//! (the handler skeleton; `client_handler.rs`). The wire vehicle is
|
||||||
//! the in-process fake `sshd` in `tests/common/sshd_double.rs` — the
|
//! the in-process fake `sshd` in `tests/common/sshd_double.rs`: the
|
||||||
//! client direction dials vanilla servers, so the loopback harness
|
//! client direction dials vanilla servers, so the loopback harness
|
||||||
//! (the *server*-direction vehicle) only hosts the crate-to-crate
|
//! (the *server*-direction vehicle) only hosts the crate-to-crate
|
||||||
//! smoke (see the task's smoke-posture record).
|
//! smoke (see the task's smoke-posture record).
|
||||||
@@ -102,7 +102,7 @@ use crate::client_handler::ClientHandler;
|
|||||||
use crate::config::{DialConfig, Limits};
|
use crate::config::{DialConfig, Limits};
|
||||||
use crate::error::DialError;
|
use crate::error::DialError;
|
||||||
|
|
||||||
/// The authenticated dial — the client direction's entry point
|
/// The authenticated dial, the client direction's entry point
|
||||||
/// (`client.md` §The dial surface): see the module docs for the
|
/// (`client.md` §The dial surface): see the module docs for the
|
||||||
/// four-step sequence and their fail-closed outcomes.
|
/// four-step sequence and their fail-closed outcomes.
|
||||||
pub async fn dial(config: DialConfig) -> Result<SshConnection, DialError> {
|
pub async fn dial(config: DialConfig) -> Result<SshConnection, DialError> {
|
||||||
@@ -149,7 +149,7 @@ pub async fn dial(config: DialConfig) -> Result<SshConnection, DialError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the dial address into its endpoint labels — the
|
/// Parses the dial address into its endpoint labels, the
|
||||||
/// [`PresentedHostKey`](crate::host_key::PresentedHostKey)'s
|
/// [`PresentedHostKey`](crate::host_key::PresentedHostKey)'s
|
||||||
/// `host`/`port` decision context. Malformed targets refuse before
|
/// `host`/`port` decision context. Malformed targets refuse before
|
||||||
/// any connection ([`DialError::Address`]): plain `host:port` and the
|
/// any connection ([`DialError::Address`]): plain `host:port` and the
|
||||||
@@ -177,7 +177,7 @@ fn parse_dial_address(address: &str) -> Result<(String, u16), DialError> {
|
|||||||
Ok((host.to_string(), port))
|
Ok((host.to_string(), port))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [`Limits`] → russh client `Config` mapping — the bounded knobs
|
/// The [`Limits`] → russh client `Config` mapping: the bounded knobs
|
||||||
/// verbatim, the wall clock always on, everything else at russh's own
|
/// verbatim, the wall clock always on, everything else at russh's own
|
||||||
/// default posture (see the module docs). The mapping is the dial
|
/// default posture (see the module docs). The mapping is the dial
|
||||||
/// task's `bounded-config wiring observable`;
|
/// task's `bounded-config wiring observable`;
|
||||||
@@ -199,15 +199,15 @@ pub fn russh_client_config(limits: &Limits) -> russh::client::Config {
|
|||||||
build_client_config(limits)
|
build_client_config(limits)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The authenticated connection handle — the type every later
|
/// The authenticated connection handle, the type every later
|
||||||
/// wave-4 op rides (`client.md`: "the authenticated connection
|
/// wave-4 op rides (`client.md`: "the authenticated connection
|
||||||
/// handle"): the russh `client::Handle` wrapped with the crate's
|
/// handle"): the russh `client::Handle` wrapped with the crate's
|
||||||
/// surface. The remote end is authenticated (publickey-only, single
|
/// surface. The remote end is authenticated (publickey-only, single
|
||||||
/// attempt); the dial key is gone (its lifetime is the dial's —
|
/// attempt); the dial key is gone (its lifetime is the dial's;
|
||||||
/// everything after rides the wire, never the key).
|
/// everything after rides the wire, never the key).
|
||||||
///
|
///
|
||||||
/// The russh handle, budgets and registry ride one shared inner
|
/// The russh handle, budgets and registry ride one shared inner
|
||||||
/// (behind the `Arc`) — the sharing the `client_accept` module's
|
/// (behind the `Arc`), the sharing the `client_accept` module's
|
||||||
/// listener handles need for their independent `cancel()` calls; the
|
/// listener handles need for their independent `cancel()` calls; the
|
||||||
/// wire-visible teardowns stay explicit calls on top of that shared
|
/// wire-visible teardowns stay explicit calls on top of that shared
|
||||||
/// stack (the module docs' §Lifecycle posture: drop is never a
|
/// stack (the module docs' §Lifecycle posture: drop is never a
|
||||||
@@ -216,13 +216,13 @@ pub struct SshConnection {
|
|||||||
shared: Arc<ConnectionShared>,
|
shared: Arc<ConnectionShared>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The connection handle's internals — the one behind the
|
/// The connection handle's internals, the one behind the
|
||||||
/// [`SshConnection`]'s `Arc`, every field private to the crate: the
|
/// [`SshConnection`]'s `Arc`, every field private to the crate: the
|
||||||
/// wire surface is the wrapped methods, never the russh stack itself
|
/// wire surface is the wrapped methods, never the russh stack itself
|
||||||
/// (`serve.rs`'s wrapped-surface pattern mirrored client-side).
|
/// (`serve.rs`'s wrapped-surface pattern mirrored client-side).
|
||||||
///
|
///
|
||||||
/// The arc's meaning for the lifecycle boundary: the russh stack
|
/// The arc's meaning for the lifecycle boundary: the russh stack
|
||||||
/// (the runner) lives while *any* handle holds the arc — the
|
/// (the runner) lives while *any* handle holds the arc; the
|
||||||
/// connection handle's drop is the abrupt teardown only when it is
|
/// connection handle's drop is the abrupt teardown only when it is
|
||||||
/// the last holder (row-pinned), and a live `ReverseListener` (its
|
/// the last holder (row-pinned), and a live `ReverseListener` (its
|
||||||
/// `cancel()` composes on this stack) keeps the wire serving until
|
/// `cancel()` composes on this stack) keeps the wire serving until
|
||||||
@@ -231,18 +231,18 @@ pub struct SshConnection {
|
|||||||
pub(crate) struct ConnectionShared {
|
pub(crate) struct ConnectionShared {
|
||||||
/// The russh handle the connection and the listener handles
|
/// The russh handle the connection and the listener handles
|
||||||
/// compose into (its own request methods take `&self` at this
|
/// compose into (its own request methods take `&self` at this
|
||||||
/// pin — the shared arc is the concurrent-sharing shape).
|
/// pin, the shared arc is the concurrent-sharing shape).
|
||||||
pub(crate) handle: russh::client::Handle<ClientHandler>,
|
pub(crate) handle: russh::client::Handle<ClientHandler>,
|
||||||
pub(crate) limits: Limits,
|
pub(crate) limits: Limits,
|
||||||
pub(crate) address: String,
|
pub(crate) address: String,
|
||||||
pub(crate) username: String,
|
pub(crate) username: String,
|
||||||
/// The `-R` leg's acceptor registry (the dial constructs it empty;
|
/// The `-R` leg's acceptor registry (the dial constructs it empty;
|
||||||
/// the assembly installs/uninstalls across the live connection —
|
/// the assembly installs/uninstalls across the live connection;
|
||||||
/// see [`SshConnection::acceptors`] and
|
/// see [`SshConnection::acceptors`] and
|
||||||
/// [`crate::client_accept`]`'s recorded decisions.
|
/// [`crate::client_accept`]`'s recorded decisions.
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
pub(crate) acceptors: Arc<AcceptorRegistry>,
|
pub(crate) acceptors: Arc<AcceptorRegistry>,
|
||||||
/// The reverse-listen count's dedicated bound — admitted at the
|
/// The reverse-listen count's dedicated bound, admitted at the
|
||||||
/// listen rows, released at the confirmed cancels and the refused/
|
/// listen rows, released at the confirmed cancels and the refused/
|
||||||
/// wire-error paths (the cap relation recorded in
|
/// wire-error paths (the cap relation recorded in
|
||||||
/// [`crate::client_accept`]; [`crate::channel_cap`]'s mutex makes
|
/// [`crate::client_accept`]; [`crate::channel_cap`]'s mutex makes
|
||||||
@@ -271,7 +271,7 @@ impl SshConnection {
|
|||||||
|
|
||||||
/// The `-R` leg's acceptor registry (the
|
/// The `-R` leg's acceptor registry (the
|
||||||
/// `w4-reverse-listen` surface, `client_accept`'s module docs
|
/// `w4-reverse-listen` surface, `client_accept`'s module docs
|
||||||
/// carry the recorded decisions) — install/uninstall the
|
/// carry the recorded decisions); install/uninstall the
|
||||||
/// per-type acceptors across the live connection (the
|
/// per-type acceptors across the live connection (the
|
||||||
/// late-install dynamic). The dial constructs it empty: nothing
|
/// late-install dynamic). The dial constructs it empty: nothing
|
||||||
/// server-initiated is served until a slot holds, fail-closed at
|
/// server-initiated is served until a slot holds, fail-closed at
|
||||||
@@ -281,14 +281,14 @@ impl SshConnection {
|
|||||||
&self.shared.acceptors
|
&self.shared.acceptors
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The reverse-listen count's shared handle — the op rows
|
/// The reverse-listen count's shared handle, which the op rows
|
||||||
/// ([`crate::client_accept`]) read it; private to the crate.
|
/// ([`crate::client_accept`]) read; private to the crate.
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
pub(crate) fn listener_budget(&self) -> &ChannelCap {
|
pub(crate) fn listener_budget(&self) -> &ChannelCap {
|
||||||
&self.shared.listener_budget
|
&self.shared.listener_budget
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The shared inner — the listener handles' compose-into point
|
/// The shared inner, the listener handles' compose-into point
|
||||||
/// (their cancels run against the same russh stack); private to
|
/// (their cancels run against the same russh stack); private to
|
||||||
/// the crate.
|
/// the crate.
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
@@ -296,7 +296,7 @@ impl SshConnection {
|
|||||||
&self.shared
|
&self.shared
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The russh handle the op rows compose into (`w4-op-rows` — the
|
/// The russh handle the op rows compose into (`w4-op-rows`; the
|
||||||
/// method bodies live in [`crate::client_ops`], a sibling module,
|
/// method bodies live in [`crate::client_ops`], a sibling module,
|
||||||
/// so the field stays private to this module).
|
/// so the field stays private to this module).
|
||||||
#[cfg(any(feature = "session", feature = "git", feature = "tunnel"))]
|
#[cfg(any(feature = "session", feature = "git", feature = "tunnel"))]
|
||||||
@@ -306,7 +306,7 @@ impl SshConnection {
|
|||||||
&self.shared.handle
|
&self.shared.handle
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The dial's bounded-resource budget, verbatim — the op rows
|
/// The dial's bounded-resource budget, verbatim; the op rows
|
||||||
/// read the knobs they consume (the exec-command length cap
|
/// read the knobs they consume (the exec-command length cap
|
||||||
/// drives the command-carrying rows' client-mirror gate).
|
/// drives the command-carrying rows' client-mirror gate).
|
||||||
#[cfg(any(feature = "session", feature = "git"))]
|
#[cfg(any(feature = "session", feature = "git"))]
|
||||||
@@ -320,12 +320,12 @@ impl SshConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The explicit clean-close path (the lifecycle primitive,
|
/// The explicit clean-close path (the lifecycle primitive,
|
||||||
/// [`russh::Disconnect::ByApplication`] — the honest close): the
|
/// [`russh::Disconnect::ByApplication`], the honest close): the
|
||||||
/// peer observes a clean disconnect, no half-open channels —
|
/// peer observes a clean disconnect, no half-open channels;
|
||||||
/// every open channel is torn down with the session (row-pinned
|
/// every open channel is torn down with the session (row-pinned
|
||||||
/// through the server double). **Drop is not this path** (the
|
/// through the server double). **Drop is not this path** (the
|
||||||
/// module docs' §Lifecycle posture): a dropped handle releases
|
/// module docs' §Lifecycle posture): a dropped handle releases
|
||||||
/// nothing on the wire — the connection outlives the handle
|
/// nothing on the wire; the connection outlives the handle
|
||||||
/// until the peer or the wall clock ends it.
|
/// until the peer or the wall clock ends it.
|
||||||
pub async fn close(self) {
|
pub async fn close(self) {
|
||||||
let _ = self
|
let _ = self
|
||||||
@@ -373,7 +373,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* `client.md` §Correctness facts (the
|
/// - *Contract stamp:* `client.md` §Correctness facts (the
|
||||||
/// keepalive posture) + `overview.md` §Bounded-resources table —
|
/// keepalive posture) + `overview.md` §Bounded-resources table;
|
||||||
/// the defaults land in the russh mapping verbatim.
|
/// the defaults land in the russh mapping verbatim.
|
||||||
#[test]
|
#[test]
|
||||||
fn bounded_config_mapping_carries_the_default_postures() {
|
fn bounded_config_mapping_carries_the_default_postures() {
|
||||||
@@ -385,7 +385,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
config.inactivity_timeout,
|
config.inactivity_timeout,
|
||||||
Some(crate::config::DEFAULT_INACTIVITY_TIMEOUT),
|
Some(crate::config::DEFAULT_INACTIVITY_TIMEOUT),
|
||||||
"the wall clock is always set — no unbounded session"
|
"the wall clock is always set, never an unbounded session"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
config.keepalive_interval,
|
config.keepalive_interval,
|
||||||
|
|||||||
+85
-85
@@ -1,28 +1,28 @@
|
|||||||
//! The `-R` leg's client half (`w4-reverse-listen` — `client.md`
|
//! The `-R` leg's client half (`w4-reverse-listen`, `client.md`
|
||||||
//! §op rows row 4 + §The acceptor): the hub's reverse direction in
|
//! §op rows row 4 + §The acceptor): the hub's reverse direction in
|
||||||
//! family types. Two surfaces, the acceptor registry first:
|
//! family types. Two surfaces, the acceptor registry first:
|
||||||
//!
|
//!
|
||||||
//! 1. **The forward request rows** — [`SshConnection::reverse_listen`]
|
//! 1. **The forward request rows**: [`SshConnection::reverse_listen`]
|
||||||
//! / [`SshConnection::reverse_listen_cancel`] / the unix pair
|
//! / [`SshConnection::reverse_listen_cancel`] / the unix pair
|
||||||
//! (`reverse_listen_unix` / `reverse_listen_unix_cancel`): the
|
//! (`reverse_listen_unix` / `reverse_listen_unix_cancel`): the
|
||||||
//! vanilla-server listen requests, the lifecycle primitives
|
//! vanilla-server listen requests, the lifecycle primitives
|
||||||
//! (`client.md` §Lifecycle). The listens return the
|
//! (`client.md` §Lifecycle). The listens return the
|
||||||
//! **[`ReverseListener`] handles** (`w4-lifecycle` — the handles'
|
//! **[`ReverseListener`] handles** (`w4-lifecycle`, the handles'
|
||||||
//! surface: the actually-bound endpoint carried, the explicit
|
//! surface: the actually-bound endpoint carried, the explicit
|
||||||
//! `cancel()`); the stateless wire cancel forms ride beside them
|
//! `cancel()`); the stateless wire cancel forms ride beside them
|
||||||
//! (sshd's global-request cancels mirrored 1:1).
|
//! (sshd's global-request cancels mirrored 1:1).
|
||||||
//! 2. **The acceptor registry** ([`AcceptorRegistry`]) — the
|
//! 2. **The acceptor registry** ([`AcceptorRegistry`]): the
|
||||||
//! assembly-mutable, per-channel-type registry the dial surface's
|
//! assembly-mutable, per-channel-type registry the dial surface's
|
||||||
//! russh handler routes **server-initiated `forwarded-*` opens**
|
//! russh handler routes **server-initiated `forwarded-*` opens**
|
||||||
//! through. Served only under a registered acceptor for that type
|
//! through. Served only under a registered acceptor for that type
|
||||||
//! and under the `tunnel` feature; unregistered/unfeatured accepted
|
//! and under the `tunnel` feature; unregistered/unfeatured accepted
|
||||||
//! opens **decline cleanly** — never russh's accept-and-drop
|
//! opens **decline cleanly**, never russh's accept-and-drop
|
||||||
//! default (ADR-003 §4, the client-side fail-closed acceptor; the
|
//! default (ADR-003 §4, the client-side fail-closed acceptor; the
|
||||||
//! reverse-runner fact 1 disposition is ours).
|
//! reverse-runner fact 1 disposition is ours).
|
||||||
//!
|
//!
|
||||||
//! ## The recorded decisions (the task's decide + record items)
|
//! ## The recorded decisions (the task's decide + record items)
|
||||||
//!
|
//!
|
||||||
//! - **Registry shape: per-type fixed slots, not a typed map** — the
|
//! - **Registry shape: per-type fixed slots, not a typed map**: the
|
||||||
//! client direction's registry-servable type set is closed and tiny
|
//! client direction's registry-servable type set is closed and tiny
|
||||||
//! (exactly the `-R` data leg's two: `forwarded-tcpip` /
|
//! (exactly the `-R` data leg's two: `forwarded-tcpip` /
|
||||||
//! `forwarded-streamlocal`, both under `tunnel`); one trait per type
|
//! `forwarded-streamlocal`, both under `tunnel`); one trait per type
|
||||||
@@ -35,70 +35,70 @@
|
|||||||
//! `ForwardedAcceptor` vocabulary is grown, not forked: the payload
|
//! `ForwardedAcceptor` vocabulary is grown, not forked: the payload
|
||||||
//! fields are the same shapes (the open's originator fields + the
|
//! fields are the same shapes (the open's originator fields + the
|
||||||
//! channel duplex), the duplex now family-typed
|
//! channel duplex), the duplex now family-typed
|
||||||
//! ([`TunnelChannel`] — the `-L` rows' byte
|
//! ([`TunnelChannel`], the `-L` rows' byte
|
||||||
//! duplex, one type both legs ride).
|
//! duplex, one type both legs ride).
|
||||||
//! - **Install is refuse-never-replace** ([`AcceptorRegistry::
|
//! - **Install is refuse-never-replace** ([`AcceptorRegistry::
|
||||||
//! install_tcpip`]'s held-slot error): the same posture recorded
|
//! install_tcpip`]'s held-slot error): the same posture recorded
|
||||||
//! server-side on the `ListenRouter` at `w4-router-collision` —
|
//! server-side on the `ListenRouter` at `w4-router-collision`:
|
||||||
//! a same-slot second install is an assembly program error, declined
|
//! a same-slot second install is an assembly program error, declined
|
||||||
//! never a quiet displacement of a live acceptor. Uninstall is
|
//! never a quiet displacement of a live acceptor. Uninstall is
|
||||||
//! explicit, 1:1 with the install.
|
//! explicit, 1:1 with the install.
|
||||||
//! - **Late-install across a live connection** (the alkminer
|
//! - **Late-install across a live connection** (the alkminer
|
||||||
//! `install_forward_sink` dynamic — reverse-runner fact 2): the
|
//! `install_forward_sink` dynamic, reverse-runner fact 2): the
|
||||||
//! registry rides the connected [`SshConnection`]
|
//! registry rides the connected [`SshConnection`]
|
||||||
//! ([`SshConnection::acceptors`]) — the dial constructs it empty,
|
//! ([`SshConnection::acceptors`]): the dial constructs it empty,
|
||||||
//! the assembly installs/uninstalls whenever it decides. The open
|
//! the assembly installs/uninstalls whenever it decides. The open
|
||||||
//! hooks read the slot **at arrival time**, fail-closed: an open
|
//! hooks read the slot **at arrival time**, fail-closed: an open
|
||||||
//! arriving before its acceptor installs (or after an uninstall)
|
//! arriving before its acceptor installs (or after an uninstall)
|
||||||
//! is **declined at arrival, never buffered** — no unclaimed-open
|
//! is **declined at arrival, never buffered**; no unclaimed-open
|
||||||
//! queue exists anywhere in this module's posture (an unclaimed
|
//! queue exists anywhere in this module's posture (an unclaimed
|
||||||
//! open's bytes would have to be buffered unboundedly to accept-
|
//! open's bytes would have to be buffered unboundedly to accept-
|
||||||
//! then-route; the decline is the clean, bounded answer).
|
//! then-route; the decline is the clean, bounded answer).
|
||||||
//! - **The listen rows gate on the registered acceptor** — the
|
//! - **The listen rows gate on the registered acceptor**, the
|
||||||
//! *issuing*-side disposition: a listen whose data leg cannot serve
|
//! *issuing*-side disposition: a listen whose data leg cannot serve
|
||||||
//! is a half-capability (the peer stays bound, every arrival
|
//! is a half-capability (the peer stays bound, every arrival
|
||||||
//! declines), so a `reverse_listen` with no registered acceptor for
|
//! declines), so a `reverse_listen` with no registered acceptor for
|
||||||
//! the type answers
|
//! the type answers
|
||||||
//! [`OpError::NoAcceptor`](crate::OpError) **before any wire op**
|
//! [`OpError::NoAcceptor`](crate::OpError) **before any wire op**
|
||||||
//! (install-first is the recorded order; the rows never buffer an
|
//! (install-first is the recorded order; the rows never buffer an
|
||||||
//! outstanding listen against a later install — re-issuing is the
|
//! outstanding listen against a later install; re-issuing is the
|
||||||
//! assembly's retry, the listen rows are stateless global requests).
|
//! assembly's retry, the listen rows are stateless global requests).
|
||||||
//! - **Bounded bookkeeping (AGENTS.md convention 9) — the cap
|
//! - **Bounded bookkeeping (AGENTS.md convention 9): the cap
|
||||||
//! relation** (the server-side listener-counter posture as the
|
//! relation** (the server-side listener-counter posture as the
|
||||||
//! template): the registry is bounded by construction (two fixed
|
//! template): the registry is bounded by construction (two fixed
|
||||||
//! slots). The per-connection **listener count** is a dedicated
|
//! slots). The per-connection **listener count** is a dedicated
|
||||||
//! in-crate counter admitting at the listen rows and releasing at
|
//! in-crate counter admitting at the listen rows and releasing at
|
||||||
//! the confirmed cancels and the failed/wire-error paths — bounded
|
//! the confirmed cancels and the failed/wire-error paths, bounded
|
||||||
//! against the *same* `Limits::max_concurrent_channels` ceiling the
|
//! against the *same* `Limits::max_concurrent_channels` ceiling the
|
||||||
//! server direction's listener counter uses (independent counts, one
|
//! server direction's listener counter uses (independent counts, one
|
||||||
//! ceiling — never a map: the outstanding listens live at the peer;
|
//! ceiling, never a map: the outstanding listens live at the peer;
|
||||||
//! the counter exists so a dial cannot mint an unbounded listener
|
//! the counter exists so a dial cannot mint an unbounded listener
|
||||||
//! set the registry would have to serve). The per-connection
|
//! set the registry would have to serve). The per-connection
|
||||||
//! **accepted-open handoff count** is the second dedicated counter
|
//! **accepted-open handoff count** is the second dedicated counter
|
||||||
//! against the same ceiling (admitted in the dial handler's open
|
//! against the same ceiling (admitted in the dial handler's open
|
||||||
//! hook, accepted-with-the-count-held, released when the handoff
|
//! hook, accepted-with-the-count-held, released when the handoff
|
||||||
//! task ends — the acceptor's `accept` completing *or the task
|
//! task ends: the acceptor's `accept` completing *or the task
|
||||||
//! unwinding* ([`HandoffCapGuard`]: a panicking assembly acceptor
|
//! unwinding* ([`HandoffCapGuard`]: a panicking assembly acceptor
|
||||||
//! can never strand a held slot) — no accepted-open ledger anywhere
|
//! can never strand a held slot); no accepted-open ledger anywhere
|
||||||
//! else: the pump is the acceptor's from the handoff on, the
|
//! else: the pump is the acceptor's from the handoff on, the
|
||||||
//! channel's transport bounds are russh's windows, and the slot read
|
//! channel's transport bounds are russh's windows, and the slot read
|
||||||
//! is the arrival admission).
|
//! is the arrival admission).
|
||||||
//! - **The listener handles (`w4-lifecycle` — the handles'
|
//! - **The listener handles (`w4-lifecycle`, the handles'
|
||||||
//! surface)**: the listen rows return **[`ReverseListener`]**
|
//! surface)**: the listen rows return **[`ReverseListener`]**
|
||||||
//! handles — the endpoint (the actually-bound values, the cancel
|
//! handles: the endpoint (the actually-bound values, the cancel
|
||||||
//! match keys) carried, the explicit `cancel()` the wire-honest
|
//! match keys) carried, the explicit `cancel()` the wire-honest
|
||||||
//! teardown. Decisions recorded:
|
//! teardown. Decisions recorded:
|
||||||
//! (a) **no `Drop` hides a wire op** — a cancellation is async and
|
//! (a) **no `Drop` hides a wire op**: a cancellation is async and
|
||||||
//! cannot run in a sync `Drop`, so no `Drop` hook exists: the
|
//! cannot run in a sync `Drop`, so no `Drop` hook exists: the
|
||||||
//! listen **outlives a dropped handle until an explicit cancel or
|
//! listen **outlives a dropped handle until an explicit cancel or
|
||||||
//! the connection's end** (no drop-timing-only wire state —
|
//! the connection's end** (no drop-timing-only wire state,
|
||||||
//! row-pinned);
|
//! row-pinned);
|
||||||
//! (b) the handle **shares the connection's inner stack** (the
|
//! (b) the handle **shares the connection's inner stack** (the
|
||||||
//! `SshConnection`'s `Arc`-held internals — the russh request
|
//! `SshConnection`'s `Arc`-held internals; the russh request
|
||||||
//! methods take `&self` at this pin), so the handle's cancel is the
|
//! methods take `&self` at this pin), so the handle's cancel is the
|
||||||
//! same wire op the stateless forms issue;
|
//! same wire op the stateless forms issue;
|
||||||
//! (c) the **stateless cancel forms ride beside** the handles
|
//! (c) the **stateless cancel forms ride beside** the handles
|
||||||
//! (sshd's global-request cancels are stateless — a cancel by the
|
//! (sshd's global-request cancels are stateless: a cancel by the
|
||||||
//! raw strings works from anywhere); every form releases the
|
//! raw strings works from anywhere); every form releases the
|
||||||
//! listener count exactly once on a confirmed cancel (the wire
|
//! listener count exactly once on a confirmed cancel (the wire
|
||||||
//! state machine governs: the second cancel is the unknown-listener
|
//! state machine governs: the second cancel is the unknown-listener
|
||||||
@@ -107,37 +107,37 @@
|
|||||||
//! *listen* rows (`reverse_listen_unix` / `reverse_listen_unix_cancel`)
|
//! *listen* rows (`reverse_listen_unix` / `reverse_listen_unix_cancel`)
|
||||||
//! compile everywhere and
|
//! compile everywhere and
|
||||||
//! decline off-Unix before any wire op (the bind is the
|
//! decline off-Unix before any wire op (the bind is the
|
||||||
//! platform-bound operation — the door puts nothing on the wire it
|
//! platform-bound operation; the door puts nothing on the wire it
|
||||||
//! cannot track). The `forwarded-streamlocal` *open* hook is
|
//! cannot track). The `forwarded-streamlocal` *open* hook is
|
||||||
//! universal: ingestion is platform-neutral byte pumping — the
|
//! universal: ingestion is platform-neutral byte pumping; the
|
||||||
//! unix-socket leg's socket is the peer-side's.
|
//! unix-socket leg's socket is the peer-side's.
|
||||||
//! - **`OpError::ListenRefused`** — the peer's `RequestDenied` on
|
//! - **`OpError::ListenRefused`**: the peer's `RequestDenied` on
|
||||||
//! every form's decline (a denied listen, a cancel of an unknown
|
//! every form's decline (a denied listen, a cancel of an unknown
|
||||||
//! listener) — channel-scoped honesty: the connection stays usable
|
//! listener), channel-scoped honesty: the connection stays usable
|
||||||
//! (the `From` mapping's recorded producer, `error.rs`).
|
//! (the `From` mapping's recorded producer, `error.rs`).
|
||||||
//!
|
//!
|
||||||
//! ## The data leg
|
//! ## The data leg
|
||||||
//!
|
//!
|
||||||
//! Per accepted open: the hook accepts the channel and hands the
|
//! Per accepted open: the hook accepts the channel and hands the
|
||||||
//! assembly the open — the [`TunnelChannel`]
|
//! assembly the open, the [`TunnelChannel`]
|
||||||
//! duplex + the open's echo/originator fields (the payload types
|
//! duplex + the open's echo/originator fields (the payload types
|
||||||
//! below) — on a spawned task (the spawn-and-return rule's client
|
//! below), on a spawned task (the spawn-and-return rule's client
|
||||||
//! mirror: the hook runs in the packet loop and returns fast). The
|
//! mirror: the hook runs in the packet loop and returns fast). The
|
||||||
//! pump is the acceptor's from the handoff — the family handler shape
|
//! pump is the acceptor's from the handoff, the family handler shape
|
||||||
//! (alktunnels' `ForwardedHandler` recipe, POC-3 §C.4); dropping the
|
//! (alktunnels' `ForwardedHandler` recipe, POC-3 §C.4); dropping the
|
||||||
//! duplex is wire-clean (russh closes the channel).
|
//! duplex is wire-clean (russh closes the channel).
|
||||||
//!
|
//!
|
||||||
//! ## Test surface
|
//! ## Test surface
|
||||||
//!
|
//!
|
||||||
//! The wire vehicle is the fake `sshd` double grown with the
|
//! The wire vehicle is the fake `sshd` double grown with the
|
||||||
//! server-initiated-forward surfaces (`tests/common/sshd_double.rs`) —
|
//! server-initiated-forward surfaces (`tests/common/sshd_double.rs`);
|
||||||
//! the double binds **real** sockets for its script accepts, opens the
|
//! the double binds **real** sockets for its script accepts, opens the
|
||||||
//! forwarded channels toward the client per accepted connection, and
|
//! forwarded channels toward the client per accepted connection, and
|
||||||
//! pumps them (a real `ssh -R`-shaped round trip in-process, the task's
|
//! pumps them (a real `ssh -R`-shaped round trip in-process, the task's
|
||||||
//! recorded smoke posture). The full-circle alkssh-client ↔
|
//! recorded smoke posture). The full-circle alkssh-client ↔
|
||||||
//! alkssh-server-direction smoke (both `-R` legs through the alkcall
|
//! alkssh-server-direction smoke (both `-R` legs through the alkcall
|
||||||
//! fabric, the composition the assembly makes) rides the fabric
|
//! fabric, the composition the assembly makes) rides the fabric
|
||||||
//! doubles in `tests/tunnel_mapping.rs` — the same doubles the
|
//! doubles in `tests/tunnel_mapping.rs`, the same doubles the
|
||||||
//! server-side `-R` rows use; real-`sshd` `-R` is wave-5 e2e
|
//! server-side `-R` rows use; real-`sshd` `-R` is wave-5 e2e
|
||||||
//! territory, never silently dropped.
|
//! territory, never silently dropped.
|
||||||
//!
|
//!
|
||||||
@@ -163,24 +163,24 @@ use crate::client::SshConnection;
|
|||||||
use crate::error::OpError;
|
use crate::error::OpError;
|
||||||
use crate::observe;
|
use crate::observe;
|
||||||
|
|
||||||
/// An accepted server-initiated `forwarded-tcpip` open — the acceptor
|
/// An accepted server-initiated `forwarded-tcpip` open: the acceptor
|
||||||
/// registry's payload for the
|
/// registry's payload for the
|
||||||
/// [`TcpipForwardedAcceptor::accept`] call: the open's echo fields
|
/// [`TcpipForwardedAcceptor::accept`] call: the open's echo fields
|
||||||
/// (the listen endpoint the arrival belongs to — the assembly's
|
/// (the listen endpoint the arrival belongs to, the assembly's
|
||||||
/// routing key; the OpenSSH matcher parity verbatim) + the originator
|
/// routing key; the OpenSSH matcher parity verbatim) + the originator
|
||||||
/// (the arrival's peer, as the wire carried it) + the channel duplex.
|
/// (the arrival's peer, as the wire carried it) + the channel duplex.
|
||||||
pub struct ForwardedTcpip {
|
pub struct ForwardedTcpip {
|
||||||
/// The connected address the open echoed — the `reverse_listen`
|
/// The connected address the open echoed, the `reverse_listen`
|
||||||
/// wire string (sshd parity).
|
/// wire string (sshd parity).
|
||||||
pub connected_address: String,
|
pub connected_address: String,
|
||||||
/// The actually-bound port of the minted listener — for a
|
/// The actually-bound port of the minted listener; for a
|
||||||
/// `port = 0` mint this is the server-picked number, never `0`.
|
/// `port = 0` mint this is the server-picked number, never `0`.
|
||||||
pub connected_port: u32,
|
pub connected_port: u32,
|
||||||
/// The arrival's peer address (the wire originator).
|
/// The arrival's peer address (the wire originator).
|
||||||
pub originator_address: String,
|
pub originator_address: String,
|
||||||
/// The arrival's peer port (the wire originator).
|
/// The arrival's peer port (the wire originator).
|
||||||
pub originator_port: u32,
|
pub originator_port: u32,
|
||||||
/// The accepted channel's byte duplex — the acceptor owns the
|
/// The accepted channel's byte duplex; the acceptor owns the
|
||||||
/// pump from the handoff on (dropping it is wire-clean).
|
/// pump from the handoff on (dropping it is wire-clean).
|
||||||
pub stream: TunnelChannel,
|
pub stream: TunnelChannel,
|
||||||
}
|
}
|
||||||
@@ -196,14 +196,14 @@ impl std::fmt::Debug for ForwardedTcpip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An accepted server-initiated `forwarded-streamlocal` open — the
|
/// An accepted server-initiated `forwarded-streamlocal` open: the
|
||||||
/// streamlocal acceptor's payload (the echoed socket path + the
|
/// streamlocal acceptor's payload (the echoed socket path + the
|
||||||
/// channel duplex).
|
/// channel duplex).
|
||||||
pub struct ForwardedStreamlocal {
|
pub struct ForwardedStreamlocal {
|
||||||
/// The socket path the open echoed — the `reverse_listen_unix`
|
/// The socket path the open echoed, the `reverse_listen_unix`
|
||||||
/// wire string (sshd parity).
|
/// wire string (sshd parity).
|
||||||
pub socket_path: String,
|
pub socket_path: String,
|
||||||
/// The accepted channel's byte duplex — the acceptor owns the
|
/// The accepted channel's byte duplex; the acceptor owns the
|
||||||
/// pump from the handoff on (dropping it is wire-clean).
|
/// pump from the handoff on (dropping it is wire-clean).
|
||||||
pub stream: TunnelChannel,
|
pub stream: TunnelChannel,
|
||||||
}
|
}
|
||||||
@@ -216,34 +216,34 @@ impl std::fmt::Debug for ForwardedStreamlocal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `forwarded-tcpip` acceptor — the registry's per-type slot
|
/// The `forwarded-tcpip` acceptor: the registry's per-type slot
|
||||||
/// trait. `accept` runs on the spawned handoff task (never the
|
/// trait. `accept` runs on the spawned handoff task (never the
|
||||||
/// packet loop): the assembly's pump/home of the open lives inside
|
/// packet loop): the assembly's pump/home of the open lives inside
|
||||||
/// it; the crate releases its handoff counter when the handoff task
|
/// it; the crate releases its handoff counter when the handoff task
|
||||||
/// ends — the call returning *or the task unwinding*
|
/// ends (the call returning *or the task unwinding*
|
||||||
/// (`HandoffCapGuard`) — so the family shape (hand off to a queue,
|
/// `HandoffCapGuard`), so the family shape (hand off to a queue,
|
||||||
/// return) keeps the counter's meaning "open accepted, handoff in
|
/// return) keeps the counter's meaning "open accepted, handoff in
|
||||||
/// flight".
|
/// flight".
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait TcpipForwardedAcceptor: Send + Sync {
|
pub trait TcpipForwardedAcceptor: Send + Sync {
|
||||||
/// Serves one accepted open. Never invoked for an unregistered
|
/// Serves one accepted open. Never invoked for an unregistered
|
||||||
/// type (the registry gate is first); the open is already accepted
|
/// type (the registry gate is first); the open is already accepted
|
||||||
/// on the wire — refusing one from here is a *data-plane* choice
|
/// on the wire; refusing one from here is a *data-plane* choice
|
||||||
/// (drop the duplex; the channel closes on it), never a wire
|
/// (drop the duplex; the channel closes on it), never a wire
|
||||||
/// open-decline.
|
/// open-decline.
|
||||||
async fn accept(&self, open: ForwardedTcpip);
|
async fn accept(&self, open: ForwardedTcpip);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `forwarded-streamlocal` acceptor — the unix slot's trait
|
/// The `forwarded-streamlocal` acceptor: the unix slot's trait
|
||||||
/// (see [`TcpipForwardedAcceptor`] for the call-shape contract).
|
/// (see [`TcpipForwardedAcceptor`] for the call-shape contract).
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait StreamlocalForwardedAcceptor: Send + Sync {
|
pub trait StreamlocalForwardedAcceptor: Send + Sync {
|
||||||
/// Serves one accepted open (platform-neutral pumping — see the
|
/// Serves one accepted open (platform-neutral pumping; see the
|
||||||
/// module docs' unix-gate record).
|
/// module docs' unix-gate record).
|
||||||
async fn accept(&self, open: ForwardedStreamlocal);
|
async fn accept(&self, open: ForwardedStreamlocal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The held-slot refusal (`AcceptorRegistry::install_*`) — the
|
/// The held-slot refusal (`AcceptorRegistry::install_*`): the
|
||||||
/// refuse-never-replace posture (the server-side `ListenRouter`'s
|
/// refuse-never-replace posture (the server-side `ListenRouter`'s
|
||||||
/// recorded sibling): the slot already holds a live acceptor; a second
|
/// recorded sibling): the slot already holds a live acceptor; a second
|
||||||
/// install is the assembly's program error, declined, never a quiet
|
/// install is the assembly's program error, declined, never a quiet
|
||||||
@@ -252,7 +252,7 @@ pub trait StreamlocalForwardedAcceptor: Send + Sync {
|
|||||||
#[error("an acceptor is already registered for this channel type")]
|
#[error("an acceptor is already registered for this channel type")]
|
||||||
pub struct AcceptorHeld;
|
pub struct AcceptorHeld;
|
||||||
|
|
||||||
/// The assembly-mutable, per-channel-type acceptor registry — the
|
/// The assembly-mutable, per-channel-type acceptor registry: the
|
||||||
/// `-R` data leg's fail-closed gate (see the module docs for the
|
/// `-R` data leg's fail-closed gate (see the module docs for the
|
||||||
/// recorded decisions). Constructed empty at the dial
|
/// recorded decisions). Constructed empty at the dial
|
||||||
/// ([`SshConnection::acceptors`], the live-connection accessor);
|
/// ([`SshConnection::acceptors`], the live-connection accessor);
|
||||||
@@ -263,7 +263,7 @@ pub struct AcceptorRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AcceptorRegistry {
|
impl AcceptorRegistry {
|
||||||
/// The empty registry — the dial's construct (nothing is served
|
/// The empty registry, the dial's construct (nothing is served
|
||||||
/// until an acceptor installs; ADR-003 §4's fail-closed default).
|
/// until an acceptor installs; ADR-003 §4's fail-closed default).
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -273,7 +273,7 @@ impl AcceptorRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Installs the `forwarded-tcpip` acceptor. Refuse-never-replace:
|
/// Installs the `forwarded-tcpip` acceptor. Refuse-never-replace:
|
||||||
/// a held slot declines ([`AcceptorHeld`]) — uninstall first if a
|
/// a held slot declines ([`AcceptorHeld`]); uninstall first if a
|
||||||
/// replacement is meant (the late-install toggle's other half).
|
/// replacement is meant (the late-install toggle's other half).
|
||||||
pub fn install_tcpip(
|
pub fn install_tcpip(
|
||||||
&self,
|
&self,
|
||||||
@@ -287,7 +287,7 @@ impl AcceptorRegistry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uninstalls the `forwarded-tcpip` acceptor — `true` when one was
|
/// Uninstalls the `forwarded-tcpip` acceptor: `true` when one was
|
||||||
/// held (the toggle's honest report). In-flight opens already
|
/// held (the toggle's honest report). In-flight opens already
|
||||||
/// accepted keep pumping (their handoff happened); arrivals after
|
/// accepted keep pumping (their handoff happened); arrivals after
|
||||||
/// this decline at arrival time (fail-closed, no buffering).
|
/// this decline at arrival time (fail-closed, no buffering).
|
||||||
@@ -315,7 +315,7 @@ impl AcceptorRegistry {
|
|||||||
self.streamlocal.lock().take().is_some()
|
self.streamlocal.lock().take().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The arrival-time slot reads (the open hooks' gates) — cloned
|
/// The arrival-time slot reads (the open hooks' gates), cloned
|
||||||
/// out so the spawned handoff task owns its acceptor handle.
|
/// out so the spawned handoff task owns its acceptor handle.
|
||||||
pub(crate) fn tcpip_acceptor(&self) -> Option<Arc<dyn TcpipForwardedAcceptor>> {
|
pub(crate) fn tcpip_acceptor(&self) -> Option<Arc<dyn TcpipForwardedAcceptor>> {
|
||||||
self.tcpip.lock().clone()
|
self.tcpip.lock().clone()
|
||||||
@@ -345,7 +345,7 @@ impl std::fmt::Debug for AcceptorRegistry {
|
|||||||
// The server-initiated opens' handoff (the dial handler's routed paths)
|
// The server-initiated opens' handoff (the dial handler's routed paths)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// The handoff counter's panic-safe release — the accepted open's count
|
/// The handoff counter's panic-safe release: the accepted open's count
|
||||||
/// releases exactly at the handoff task's end, on *completion or
|
/// releases exactly at the handoff task's end, on *completion or
|
||||||
/// unwind*: a panicking assembly acceptor (or any panic in the handoff
|
/// unwind*: a panicking assembly acceptor (or any panic in the handoff
|
||||||
/// body) can never strand a held slot past the task. Saturating under
|
/// body) can never strand a held slot past the task. Saturating under
|
||||||
@@ -358,14 +358,14 @@ impl Drop for HandoffCapGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The accepted `forwarded-tcpip` open's handoff task — the
|
/// The accepted `forwarded-tcpip` open's handoff task: the
|
||||||
/// spawn-and-return rule's client mirror (ADR-006 decision 4; the
|
/// spawn-and-return rule's client mirror (ADR-006 decision 4; the
|
||||||
/// hook runs in the packet loop and returns fast): accepts the open on
|
/// hook runs in the packet loop and returns fast): accepts the open on
|
||||||
/// the wire, converts the channel to the family duplex, delivers the
|
/// the wire, converts the channel to the family duplex, delivers the
|
||||||
/// payload to the assembly's acceptor, and releases the handoff
|
/// payload to the assembly's acceptor, and releases the handoff
|
||||||
/// counter when the handoff task ends — the acceptor's `accept`
|
/// counter when the handoff task ends: the acceptor's `accept`
|
||||||
/// completing *or the task unwinding* ([`HandoffCapGuard`]; the
|
/// completing *or the task unwinding* ([`HandoffCapGuard`]; the
|
||||||
/// recorded release site — the count's meaning is "accepted, handoff
|
/// recorded release site; the count's meaning is "accepted, handoff
|
||||||
/// in flight").
|
/// in flight").
|
||||||
#[cfg_attr(feature = "tunnel", allow(clippy::too_many_arguments))]
|
#[cfg_attr(feature = "tunnel", allow(clippy::too_many_arguments))]
|
||||||
pub(crate) async fn handoff_forwarded_tcpip(
|
pub(crate) async fn handoff_forwarded_tcpip(
|
||||||
@@ -396,7 +396,7 @@ pub(crate) async fn handoff_forwarded_tcpip(
|
|||||||
observe::serving_path_ended("forwarded-tcpip", channel_number, None);
|
observe::serving_path_ended("forwarded-tcpip", channel_number, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The accepted `forwarded-streamlocal` open's handoff task — the
|
/// The accepted `forwarded-streamlocal` open's handoff task: the
|
||||||
/// unix slot's mirror of [`handoff_forwarded_tcpip`].
|
/// unix slot's mirror of [`handoff_forwarded_tcpip`].
|
||||||
pub(crate) async fn handoff_forwarded_streamlocal(
|
pub(crate) async fn handoff_forwarded_streamlocal(
|
||||||
acceptor: Arc<dyn StreamlocalForwardedAcceptor>,
|
acceptor: Arc<dyn StreamlocalForwardedAcceptor>,
|
||||||
@@ -421,10 +421,10 @@ pub(crate) async fn handoff_forwarded_streamlocal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// The listener handles (w4-lifecycle — the handles' surface)
|
// The listener handles (w4-lifecycle, the handles' surface)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// The endpoint a reverse listener is bound to — what a
|
/// The endpoint a reverse listener is bound to: what a
|
||||||
/// [`ReverseListener`] carries for its assembly-side use (the
|
/// [`ReverseListener`] carries for its assembly-side use (the
|
||||||
/// alkminer restore flow: the *actually bound* values to persist and
|
/// alkminer restore flow: the *actually bound* values to persist and
|
||||||
/// rebind) and the cancel forms' raw wire match key (sshd parity: a
|
/// rebind) and the cancel forms' raw wire match key (sshd parity: a
|
||||||
@@ -439,8 +439,8 @@ pub enum ReverseEndpoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ReverseEndpoint {
|
impl ReverseEndpoint {
|
||||||
/// The actually-bound port — the tcp shape's; `None` for the unix
|
/// The actually-bound port, the tcp shape's; `None` for the unix
|
||||||
/// shape (no port semantics — the cancel matches by the path).
|
/// shape (no port semantics; the cancel matches by the path).
|
||||||
pub fn bound_port(&self) -> Option<u32> {
|
pub fn bound_port(&self) -> Option<u32> {
|
||||||
match self {
|
match self {
|
||||||
Self::Tcp { port, .. } => Some(*port),
|
Self::Tcp { port, .. } => Some(*port),
|
||||||
@@ -449,14 +449,14 @@ impl ReverseEndpoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A returned forward listener — the `-R` leg's **listener handle**
|
/// A returned forward listener: the `-R` leg's **listener handle**
|
||||||
/// (the lifecycle primitive: every listener explicitly cancellable):
|
/// (the lifecycle primitive: every listener explicitly cancellable):
|
||||||
/// the bound endpoint carried (see [`ReverseEndpoint`]; the
|
/// the bound endpoint carried (see [`ReverseEndpoint`]; the
|
||||||
/// assembly's own record for a restore/rebind policy — the crate
|
/// assembly's own record for a restore/rebind policy; the crate
|
||||||
/// ships none, OQ-12), and the explicit `cancel()`.
|
/// ships none, OQ-12), and the explicit `cancel()`.
|
||||||
///
|
///
|
||||||
/// **Drop frees nothing on the wire** (the recorded decision in this
|
/// **Drop frees nothing on the wire** (the recorded decision in this
|
||||||
/// module's docs): there is no `Drop` hook — a dropped handle leaves
|
/// module's docs): there is no `Drop` hook, so a dropped handle leaves
|
||||||
/// the peer's listen serving until an explicit `cancel()` or the
|
/// the peer's listen serving until an explicit `cancel()` or the
|
||||||
/// connection's end removes it. The handle does not borrow the
|
/// connection's end removes it. The handle does not borrow the
|
||||||
/// connection: the cancel composes on the shared wire stack (the
|
/// connection: the cancel composes on the shared wire stack (the
|
||||||
@@ -469,18 +469,18 @@ pub struct ReverseListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ReverseListener {
|
impl ReverseListener {
|
||||||
/// The bound endpoint — the actually-bound address/port or socket
|
/// The bound endpoint, the actually-bound address/port or socket
|
||||||
/// path (never the requested `0` for a mint: the peer's answer is
|
/// path (never the requested `0` for a mint: the peer's answer is
|
||||||
/// the surfaced number).
|
/// the surfaced number).
|
||||||
pub fn endpoint(&self) -> &ReverseEndpoint {
|
pub fn endpoint(&self) -> &ReverseEndpoint {
|
||||||
&self.endpoint
|
&self.endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The explicit cancellation — the wire teardown: the peer
|
/// The explicit cancellation, the wire teardown: the peer
|
||||||
/// removes the matching listen (the endpoint's raw wire strings,
|
/// removes the matching listen (the endpoint's raw wire strings,
|
||||||
/// sshd parity; the accept loop ends, the bound port frees) and
|
/// sshd parity; the accept loop ends, the bound port frees) and
|
||||||
/// the confirmed cancel releases the connection's listener count.
|
/// the confirmed cancel releases the connection's listener count.
|
||||||
/// The peer's refusal (an unknown listener — e.g. the already-
|
/// The peer's refusal (an unknown listener, e.g. the already-
|
||||||
/// cancelled, or a cancelled by the connection's end itself) is
|
/// cancelled, or a cancelled by the connection's end itself) is
|
||||||
/// [`OpError::ListenRefused`](crate::OpError). The handle is
|
/// [`OpError::ListenRefused`](crate::OpError). The handle is
|
||||||
/// consumed (one cancel through it); a later cancel by the same
|
/// consumed (one cancel through it); a later cancel by the same
|
||||||
@@ -511,10 +511,10 @@ impl std::fmt::Debug for ReverseListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The tcp cancel's shared path — the stateless wire form and the
|
/// The tcp cancel's shared path: the stateless wire form and the
|
||||||
/// listener handle's `cancel` compose into it: the wire op, then the
|
/// listener handle's `cancel` compose into it: the wire op, then the
|
||||||
/// confirmed release (exactly once: a second cancel hits the wire's
|
/// confirmed release (exactly once: a second cancel hits the wire's
|
||||||
/// unknown-listener refusal, no release — budget-safe by the wire's
|
/// unknown-listener refusal, no release; budget-safe by the wire's
|
||||||
/// own state machine).
|
/// own state machine).
|
||||||
async fn cancel_reverse_tcp(
|
async fn cancel_reverse_tcp(
|
||||||
shared: &crate::client::ConnectionShared,
|
shared: &crate::client::ConnectionShared,
|
||||||
@@ -531,7 +531,7 @@ async fn cancel_reverse_tcp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The unix cancel's shared path (the twin; the runtime-Unix gate
|
/// The unix cancel's shared path (the twin; the runtime-Unix gate
|
||||||
/// answers before the wire — see the module docs' gate record).
|
/// answers before the wire; see the module docs' gate record).
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
async fn cancel_reverse_unix(
|
async fn cancel_reverse_unix(
|
||||||
shared: &crate::client::ConnectionShared,
|
shared: &crate::client::ConnectionShared,
|
||||||
@@ -552,28 +552,28 @@ async fn cancel_reverse_unix(
|
|||||||
|
|
||||||
impl SshConnection {
|
impl SshConnection {
|
||||||
/// The reverse-listen row (`-R`): asks the vanilla server to listen
|
/// The reverse-listen row (`-R`): asks the vanilla server to listen
|
||||||
/// on (address, port) and returns the **actually-bound port** —
|
/// on (address, port) and returns the **actually-bound port**;
|
||||||
/// for `port = 0` this is the peer-picked number (`client.md`
|
/// for `port = 0` this is the peer-picked number (`client.md`
|
||||||
/// §Lifecycle: the bound-port primitive, the port-0 mint); for an
|
/// §Lifecycle: the bound-port primitive, the port-0 mint); for an
|
||||||
/// explicit port the wire's success reply carries no number (russh/
|
/// explicit port the wire's success reply carries no number (russh/
|
||||||
/// sshd parity — the reply's data only exists for mints) and the
|
/// sshd parity; the reply's data only exists for mints) and the
|
||||||
/// bound port *is* the requested one, which the row surfaces.
|
/// bound port *is* the requested one, which the row surfaces.
|
||||||
///
|
///
|
||||||
/// Gates, both **before any wire op**: the `forwarded-tcpip`
|
/// Gates, both **before any wire op**: the `forwarded-tcpip`
|
||||||
/// acceptor must be registered
|
/// acceptor must be registered
|
||||||
/// ([`OpError::NoAcceptor`](crate::OpError) — a listen whose data
|
/// ([`OpError::NoAcceptor`](crate::OpError), a listen whose data
|
||||||
/// leg cannot serve is a half-capability) and the connection's
|
/// leg cannot serve is a half-capability) and the connection's
|
||||||
/// listener count must have room
|
/// listener count must have room
|
||||||
/// ([`OpError::ListenCapExceeded`](crate::OpError) — the
|
/// ([`OpError::ListenCapExceeded`](crate::OpError), the
|
||||||
/// dedicated counter sharing
|
/// dedicated counter sharing
|
||||||
/// [`Limits::max_concurrent_channels`](crate::Limits); see the
|
/// [`Limits::max_concurrent_channels`](crate::Limits); see the
|
||||||
/// module docs' cap relation). The peer's refusal is
|
/// module docs' cap relation). The peer's refusal is
|
||||||
/// [`OpError::ListenRefused`](crate::OpError) (the wire's
|
/// [`OpError::ListenRefused`](crate::OpError) (the wire's
|
||||||
/// `RequestDenied`) and releases the count; the connection stays
|
/// `RequestDenied`) and releases the count; the connection stays
|
||||||
/// usable after every refusal class. The listener is explicitly
|
/// usable after every refusal class. The listener is explicitly
|
||||||
/// cancellable — the returned [`ReverseListener`] handle's
|
/// cancellable, via the returned [`ReverseListener`] handle's
|
||||||
/// `cancel()` (the wire-honest teardown; a cancel is never
|
/// `cancel()` (the wire-honest teardown; a cancel is never
|
||||||
/// triggered by a drop — the module docs' recorded decision).
|
/// triggered by a drop; the module docs' recorded decision).
|
||||||
pub async fn reverse_listen(
|
pub async fn reverse_listen(
|
||||||
&self,
|
&self,
|
||||||
address: impl Into<String>,
|
address: impl Into<String>,
|
||||||
@@ -608,10 +608,10 @@ impl SshConnection {
|
|||||||
|
|
||||||
/// The reverse-listen cancel row (`-R` teardown): the explicit
|
/// The reverse-listen cancel row (`-R` teardown): the explicit
|
||||||
/// cancellation of one bound listener by the raw wire strings
|
/// cancellation of one bound listener by the raw wire strings
|
||||||
/// (sshd parity) — the **stateless** wire form beside the
|
/// (sshd parity), the **stateless** wire form beside the
|
||||||
/// [`ReverseListener`] handles' `cancel()` (compose into the same
|
/// [`ReverseListener`] handles' `cancel()` (compose into the same
|
||||||
/// shared path; a confirmed cancel releases the listener count —
|
/// shared path; a confirmed cancel releases the listener count
|
||||||
/// never twice, the wire's unknown-listener refusal governs the
|
/// (never twice; the wire's unknown-listener refusal governs the
|
||||||
/// repeats). The peer's refusal (unknown listener) is
|
/// repeats). The peer's refusal (unknown listener) is
|
||||||
/// [`OpError::ListenRefused`](crate::OpError).
|
/// [`OpError::ListenRefused`](crate::OpError).
|
||||||
pub async fn reverse_listen_cancel(
|
pub async fn reverse_listen_cancel(
|
||||||
@@ -630,7 +630,7 @@ impl SshConnection {
|
|||||||
/// [`SshConnection::reverse_listen`] for the
|
/// [`SshConnection::reverse_listen`] for the
|
||||||
/// `forwarded-streamlocal` slot, plus the **runtime-Unix gate**
|
/// `forwarded-streamlocal` slot, plus the **runtime-Unix gate**
|
||||||
/// (OQ-11: off-Unix, [`OpError::UnixUnavailable`](crate::OpError)
|
/// (OQ-11: off-Unix, [`OpError::UnixUnavailable`](crate::OpError)
|
||||||
/// before any wire op — the bind is the platform-bound operation;
|
/// before any wire op; the bind is the platform-bound operation,
|
||||||
/// the channel ingestion is platform-neutral).
|
/// the channel ingestion is platform-neutral).
|
||||||
#[cfg_attr(not(unix), allow(unused_variables))]
|
#[cfg_attr(not(unix), allow(unused_variables))]
|
||||||
pub async fn reverse_listen_unix(
|
pub async fn reverse_listen_unix(
|
||||||
@@ -667,7 +667,7 @@ impl SshConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The streamlocal cancel row (matches by the socket path, sshd
|
/// The streamlocal cancel row (matches by the socket path, sshd
|
||||||
/// parity — see [`SshConnection::reverse_listen_cancel`]; composes
|
/// parity, see [`SshConnection::reverse_listen_cancel`]; composes
|
||||||
/// into the same shared path as the unix handles' `cancel()`).
|
/// into the same shared path as the unix handles' `cancel()`).
|
||||||
#[cfg_attr(not(unix), allow(unused_variables))]
|
#[cfg_attr(not(unix), allow(unused_variables))]
|
||||||
pub async fn reverse_listen_unix_cancel(
|
pub async fn reverse_listen_unix_cancel(
|
||||||
@@ -708,7 +708,7 @@ mod tests {
|
|||||||
async fn accept(&self, _open: ForwardedStreamlocal) {}
|
async fn accept(&self, _open: ForwardedStreamlocal) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the recorded refuse-never-replace posture —
|
/// - *Contract stamp:* the recorded refuse-never-replace posture:
|
||||||
/// a held slot declines the second install (never a quiet
|
/// a held slot declines the second install (never a quiet
|
||||||
/// displacement of a live acceptor); uninstall frees it.
|
/// displacement of a live acceptor); uninstall frees it.
|
||||||
#[test]
|
#[test]
|
||||||
@@ -736,7 +736,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the two slots are independent (the
|
/// - *Contract stamp:* the two slots are independent (the
|
||||||
/// per-type-registry decision) — the empty streamlocal slot
|
/// per-type-registry decision); the empty streamlocal slot
|
||||||
/// stays unfeatured while the tcpip slot serves.
|
/// stays unfeatured while the tcpip slot serves.
|
||||||
#[test]
|
#[test]
|
||||||
fn the_slots_are_independent() {
|
fn the_slots_are_independent() {
|
||||||
@@ -789,7 +789,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the handoff counter's panic-safe release
|
/// - *Contract stamp:* the handoff counter's panic-safe release
|
||||||
/// (the bounded-bookkeeping cap relation) — the
|
/// (the bounded-bookkeeping cap relation); the
|
||||||
/// [`HandoffCapGuard`] releases on unwind, so a panicking
|
/// [`HandoffCapGuard`] releases on unwind, so a panicking
|
||||||
/// assembly acceptor can never strand a held slot past the
|
/// assembly acceptor can never strand a held slot past the
|
||||||
/// handoff task.
|
/// handoff task.
|
||||||
|
|||||||
+29
-29
@@ -1,4 +1,4 @@
|
|||||||
//! The per-connection **client** handler — the dial surface's russh
|
//! The per-connection **client** handler, the dial surface's russh
|
||||||
//! `client::Handler` impl (`w4-dial`), the mirror of
|
//! `client::Handler` impl (`w4-dial`), the mirror of
|
||||||
//! [`crate::handler`]'s server-direction handler.
|
//! [`crate::handler`]'s server-direction handler.
|
||||||
//!
|
//!
|
||||||
@@ -9,46 +9,46 @@
|
|||||||
//! the crate's single fingerprint path ([`crate::fingerprint`]),
|
//! the crate's single fingerprint path ([`crate::fingerprint`]),
|
||||||
//! labels it with the dial target (host + port, parsed from the
|
//! labels it with the dial target (host + port, parsed from the
|
||||||
//! dial address), and hands the whole [`PresentedHostKey`] to the
|
//! dial address), and hands the whole [`PresentedHostKey`] to the
|
||||||
//! injected [`HostKeyPolicy`] — the verdict rides straight through
|
//! injected [`HostKeyPolicy`], and the verdict rides straight
|
||||||
//! (`Ok(policy.decide(..).await)`). The crate is policy-blind; the
|
//! through (`Ok(policy.decide(..).await)`). The crate is policy-blind; the
|
||||||
//! built-in [`RejectAll`](crate::RejectAll) verdict makes every dial
|
//! built-in [`RejectAll`](crate::RejectAll) verdict makes every dial
|
||||||
//! refuse; a `false` verdict is the decline disposition russh
|
//! refuse; a `false` verdict is the decline disposition russh
|
||||||
//! surfaces as its `UnknownKey` wire class
|
//! surfaces as its `UnknownKey` wire class
|
||||||
//! ([`DialError::HostKeyRejected`](crate::DialError)
|
//! ([`DialError::HostKeyRejected`](crate::DialError)),
|
||||||
//! — never an accept, never a retry).
|
//! never an accept, never a retry).
|
||||||
//! 2. **Server-initiated channel opens declined cleanly** (ADR-003 §4,
|
//! 2. **Server-initiated channel opens declined cleanly** (ADR-003 §4,
|
||||||
//! the client-side symmetry rule): russh's default posture for every
|
//! the client-side symmetry rule): russh's default posture for every
|
||||||
//! `server_channel_open_*` hook is accept-and-drop — a
|
//! `server_channel_open_*` hook is accept-and-drop, a
|
||||||
//! half-capability the family forbids. Every hook is overridden
|
//! half-capability the family forbids. Every hook is overridden
|
||||||
//! explicit-over-inherited. The `forwarded-tcpip` /
|
//! explicit-over-inherited. The `forwarded-tcpip` /
|
||||||
//! `forwarded-streamlocal` hooks (the `-R` data leg's types) route
|
//! `forwarded-streamlocal` hooks (the `-R` data leg's types) route
|
||||||
//! through the **acceptor registry** ([`crate::client_accept`] —
|
//! through the **acceptor registry** ([`crate::client_accept`],
|
||||||
//! the `w4-reverse-listen` surface): served only under a registered
|
//! the `w4-reverse-listen` surface): served only under a registered
|
||||||
//! acceptor for that type and under the `tunnel` feature (the
|
//! acceptor for that type and under the `tunnel` feature (the
|
||||||
//! [`with_tunnel_surface`]'s tunnel surface
|
//! [`with_tunnel_surface`]'s tunnel surface
|
||||||
//! wiring; a build without `tunnel` has no registry — every hook
|
//! wiring; a build without `tunnel` has no registry, so every hook
|
||||||
//! declines); unregistered/unfeatured opens answer
|
//! declines); unregistered/unfeatured opens answer
|
||||||
//! [`ChannelOpenFailure::AdministrativelyProhibited`] — a clean
|
//! [`ChannelOpenFailure::AdministrativelyProhibited`], a clean
|
||||||
//! wire-visible refusal the peer observes as an
|
//! wire-visible refusal the peer observes as an
|
||||||
//! unconfirmed open, never an accepted-then-dropped channel. The
|
//! unconfirmed open, never an accepted-then-dropped channel. The
|
||||||
//! accepted open's handoff spawns (the spawn-and-return rule's
|
//! accepted open's handoff spawns (the spawn-and-return rule's
|
||||||
//! client mirror): the hook accepts and returns fast, the duplex +
|
//! client mirror): the hook accepts and returns fast, the duplex +
|
||||||
//! the open's fields ride the payload to the assembly's acceptor
|
//! the open's fields ride the payload to the assembly's acceptor
|
||||||
//! call on its own task. Every other type has no registry slot —
|
//! call on its own task. Every other type has no registry slot:
|
||||||
//! the unknown-type gate
|
//! the unknown-type gate
|
||||||
//! ([`Handler::should_accept_unknown_server_channel`]) answers
|
//! ([`Handler::should_accept_unknown_server_channel`]) answers
|
||||||
//! `false` (the hook's reject-before-open form; its `_unknown`
|
//! `false` (the hook's reject-before-open form; its `_unknown`
|
||||||
//! sibling stays a defensive reject — wire-reachable only if that
|
//! sibling stays a defensive reject, wire-reachable only if that
|
||||||
//! gate ever changed, also fail-closed), and the never-feature rows
|
//! gate ever changed, also fail-closed), and the never-feature rows
|
||||||
//! (x11, agent) decline at any configuration.
|
//! (x11, agent) decline at any configuration.
|
||||||
//!
|
//!
|
||||||
//! The accepted-open handoff carries a **bounded bookkeeping**
|
//! The accepted-open handoff carries a **bounded bookkeeping**
|
||||||
//! counter (`AGENTS.md` convention 9, the cap relation recorded in
|
//! counter (`AGENTS.md` convention 9, the cap relation recorded in
|
||||||
//! [`crate::client_accept`]): a dedicated per-connection counter —
|
//! [`crate::client_accept`]): a dedicated per-connection counter
|
||||||
//! sharing the dial's `Limits::max_concurrent_channels` ceiling the
|
//! (sharing the dial's `Limits::max_concurrent_channels` ceiling the
|
||||||
//! server direction's per-session cap uses — admitted in the hook
|
//! server direction's per-session cap uses), admitted in the hook
|
||||||
//! before the accept, released when the handoff task ends (the
|
//! before the accept, released when the handoff task ends (the
|
||||||
//! acceptor's call completing *or the task unwinding* —
|
//! acceptor's call completing *or the task unwinding*;
|
||||||
//! [`crate::client_accept`]'s `HandoffCapGuard` panic-safety); a full
|
//! [`crate::client_accept`]'s `HandoffCapGuard` panic-safety); a full
|
||||||
//! counter declines the arrival cleanly
|
//! counter declines the arrival cleanly
|
||||||
//! (`cap-exceeded`).
|
//! (`cap-exceeded`).
|
||||||
@@ -79,7 +79,7 @@ use crate::{DialError, fingerprint};
|
|||||||
/// the two forwarded hooks route through, plus the accepted-open
|
/// the two forwarded hooks route through, plus the accepted-open
|
||||||
/// handoff counter and its ceiling (the dial's
|
/// handoff counter and its ceiling (the dial's
|
||||||
/// `Limits::max_concurrent_channels`; released when the handoff task
|
/// `Limits::max_concurrent_channels`; released when the handoff task
|
||||||
/// ends — completion or unwind, `HandoffCapGuard`'s panic-safety).
|
/// ends, completion or unwind, `HandoffCapGuard`'s panic-safety).
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
pub(crate) struct TunnelSurface {
|
pub(crate) struct TunnelSurface {
|
||||||
acceptors: Arc<AcceptorRegistry>,
|
acceptors: Arc<AcceptorRegistry>,
|
||||||
@@ -87,7 +87,7 @@ pub(crate) struct TunnelSurface {
|
|||||||
open_cap: std::sync::Arc<ChannelCap>,
|
open_cap: std::sync::Arc<ChannelCap>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The per-connection client handler the dial surface constructs —
|
/// The per-connection client handler the dial surface constructs;
|
||||||
/// see the module docs. Test surface
|
/// see the module docs. Test surface
|
||||||
/// (`#[doc(hidden)]` re-export from the crate root): the contract
|
/// (`#[doc(hidden)]` re-export from the crate root): the contract
|
||||||
/// tests drive the bare hooks, exactly the
|
/// tests drive the bare hooks, exactly the
|
||||||
@@ -96,7 +96,7 @@ pub struct ClientHandler {
|
|||||||
policy: std::sync::Arc<dyn HostKeyPolicy>,
|
policy: std::sync::Arc<dyn HostKeyPolicy>,
|
||||||
host: String,
|
host: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
/// The `-R` surface under `tunnel` (`Some` — the dial wires it;
|
/// The `-R` surface under `tunnel` (`Some`, the dial wires it;
|
||||||
/// the bare-hook tests construct without it to pin the decline-all
|
/// the bare-hook tests construct without it to pin the decline-all
|
||||||
/// default posture in the featured builds too).
|
/// default posture in the featured builds too).
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
@@ -143,10 +143,10 @@ impl russh::client::Handler for ClientHandler {
|
|||||||
|
|
||||||
/// The host-key policy's hook (OQ-05): the presented key or
|
/// The host-key policy's hook (OQ-05): the presented key or
|
||||||
/// certificate's fingerprint is computed on the crate's single
|
/// certificate's fingerprint is computed on the crate's single
|
||||||
/// fingerprint path (certificates fingerprint their inner key —
|
/// fingerprint path (certificates fingerprint their inner key;
|
||||||
/// the trust decision is the policy's either way) and the whole
|
/// the trust decision is the policy's either way) and the whole
|
||||||
/// [`PresentedHostKey`] rides the policy; the verdict is passed
|
/// [`PresentedHostKey`] rides the policy; the verdict is passed
|
||||||
/// through untouched. `Ok(false)` — russh's decline — is the
|
/// through untouched. `Ok(false)`, russh's decline, is the
|
||||||
/// disposition, never an error path; the policy's own record
|
/// disposition, never an error path; the policy's own record
|
||||||
/// carries why.
|
/// carries why.
|
||||||
async fn check_server_key(
|
async fn check_server_key(
|
||||||
@@ -186,7 +186,7 @@ impl russh::client::Handler for ClientHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The `forwarded-tcpip` hook (the `-R` data leg's gate): the
|
/// The `forwarded-tcpip` hook (the `-R` data leg's gate): the
|
||||||
/// registry's slot read at arrival time — registered + room →
|
/// registry's slot read at arrival time (registered + room →
|
||||||
/// accept and hand the assembly the open on a spawned task; no
|
/// accept and hand the assembly the open on a spawned task; no
|
||||||
/// acceptor / no room → the clean decline, never accept-and-drop.
|
/// acceptor / no room → the clean decline, never accept-and-drop.
|
||||||
async fn server_channel_open_forwarded_tcpip(
|
async fn server_channel_open_forwarded_tcpip(
|
||||||
@@ -247,10 +247,10 @@ impl russh::client::Handler for ClientHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `forwarded-streamlocal` hook — the unix-slot's mirror of
|
/// The `forwarded-streamlocal` hook, the unix-slot's mirror of
|
||||||
/// the tcpip gate (the ingestion is platform-neutral: no runtime
|
/// the tcpip gate (the ingestion is platform-neutral: no runtime
|
||||||
/// gate here; the listen rows carry the OQ-11 gate on the bind —
|
/// gate here; the listen rows carry the OQ-11 gate on the bind,
|
||||||
/// the recorded asymmetry, see [`crate::client_accept`]).
|
/// the recorded asymmetry (see [`crate::client_accept`]).
|
||||||
async fn server_channel_open_forwarded_streamlocal(
|
async fn server_channel_open_forwarded_streamlocal(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel: Channel<Msg>,
|
channel: Channel<Msg>,
|
||||||
@@ -350,8 +350,8 @@ impl russh::client::Handler for ClientHandler {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A never-feature row (ADR-003 §5): x11 has no family seam — the
|
/// A never-feature row (ADR-003 §5): x11 has no family seam, so
|
||||||
/// decline is not even registry territory.
|
/// the decline is not even registry territory.
|
||||||
async fn server_channel_open_x11(
|
async fn server_channel_open_x11(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel: Channel<Msg>,
|
channel: Channel<Msg>,
|
||||||
@@ -368,7 +368,7 @@ impl russh::client::Handler for ClientHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A never-feature row (ADR-003 §5): agent forwarding is the
|
/// A never-feature row (ADR-003 §5): agent forwarding is the
|
||||||
/// anti-pattern of the vault custody model — always declined.
|
/// anti-pattern of the vault custody model, always declined.
|
||||||
async fn server_channel_open_agent_forward(
|
async fn server_channel_open_agent_forward(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel: Channel<Msg>,
|
channel: Channel<Msg>,
|
||||||
@@ -384,7 +384,7 @@ impl russh::client::Handler for ClientHandler {
|
|||||||
|
|
||||||
/// Wire-reachable only if
|
/// Wire-reachable only if
|
||||||
/// [`russh::client::Handler::should_accept_unknown_server_channel`]
|
/// [`russh::client::Handler::should_accept_unknown_server_channel`]
|
||||||
/// ever answers `true` (it does not — the skeleton's default is
|
/// ever answers `true` (it does not; the skeleton's default is
|
||||||
/// fail-closed); the defensive reject keeps the posture honest
|
/// fail-closed); the defensive reject keeps the posture honest
|
||||||
/// even then.
|
/// even then.
|
||||||
async fn server_channel_open_unknown(
|
async fn server_channel_open_unknown(
|
||||||
@@ -415,7 +415,7 @@ mod tests {
|
|||||||
|
|
||||||
/// - *Contract stamp:* ADR-004 decision 6 posture mirror (the
|
/// - *Contract stamp:* ADR-004 decision 6 posture mirror (the
|
||||||
/// client-side: the policy is injected, the handler never
|
/// client-side: the policy is injected, the handler never
|
||||||
/// decides trust), pinned at the dial wiring — here, that the
|
/// decides trust), pinned at the dial wiring; here, that the
|
||||||
/// labels ride the construction so the policy sees
|
/// labels ride the construction so the policy sees
|
||||||
/// `PresentedHostKey { host, port }` verbatim, and the
|
/// `PresentedHostKey { host, port }` verbatim, and the
|
||||||
/// fingerprint rides the crate's single fingerprint path.
|
/// fingerprint rides the crate's single fingerprint path.
|
||||||
|
|||||||
+66
-66
@@ -1,67 +1,67 @@
|
|||||||
//! The client op rows (`w4-op-rows`) — `client.md` §op rows in code:
|
//! The client op rows (`w4-op-rows`), `client.md` §op rows in code:
|
||||||
//! the family op → vanilla translation the reverse-runner shape
|
//! the family op → vanilla translation the reverse-runner shape
|
||||||
//! drives (the hub dialed [`SshConnection`](crate::SshConnection) and
|
//! drives (the hub dialed [`SshConnection`](crate::SshConnection) and
|
||||||
//! now opens every channel itself). **Type symmetry at the seam is
|
//! now opens every channel itself). **Type symmetry at the seam is
|
||||||
//! the contract:** family-protocol types in, vanilla wire ops out, no
|
//! the contract:** family-protocol types in, vanilla wire ops out, no
|
||||||
//! vanilla-concept leaks up — the rows' inputs are the mapped
|
//! vanilla-concept leaks up; the rows' inputs are the mapped
|
||||||
//! sibling crates' types (the alktty session shapes for the exec/pty
|
//! sibling crates' types (the alktty session shapes for the exec/pty
|
||||||
//! rows, alkgit's `Service` for the git row) and the surfaces are
|
//! rows, alkgit's `Service` for the git row) and the surfaces are
|
||||||
//! crate-owned (`RemoteSession` / `TunnelChannel` / `GitPackSession`),
|
//! crate-owned (`RemoteSession` / `TunnelChannel` / `GitPackSession`),
|
||||||
//! the failure taxonomy [`OpError`](crate::OpError). All rows share
|
//! the failure taxonomy [`OpError`](crate::OpError). All rows share
|
||||||
//! the russh 0.65 mechanics POC-3 §D.3 recorded:
|
//! the russh 0.65 mechanics POC-3 §D.3 recorded:
|
||||||
//!
|
//!
|
||||||
//! - `Channel::exec()` — there is **no `request_exec` at this pin**.
|
//! - `Channel::exec()`: there is **no `request_exec` at this pin**.
|
||||||
//! - A request's Success/Failure replies arrive **on the channel
|
//! - A request's Success/Failure replies arrive **on the channel
|
||||||
//! queue via `wait()`**, not as `Err` from the request call — every
|
//! queue via `wait()`**, not as `Err` from the request call, so every
|
||||||
//! request-carrying row drains until the expected message,
|
//! request-carrying row drains until the expected message,
|
||||||
//! `continue`-ing past the interleaved rest, and maps a `Failure`
|
//! `continue`-ing past the interleaved rest, and maps a `Failure`
|
||||||
//! to [`OpError::RequestRefused`](crate::OpError). The drain is
|
//! to [`OpError::RequestRefused`](crate::OpError). The drain is
|
||||||
//! message-capped (bounded resources: a peer that never answers
|
//! message-capped (bounded resources: a peer that never answers
|
||||||
//! cannot loop it unboundedly; the *connection's* wall clock — the
|
//! cannot loop it unboundedly; the *connection's* wall clock, the
|
||||||
//! dial's always-set [`Limits::inactivity_timeout`](crate::Limits) —
|
//! dial's always-set [`Limits::inactivity_timeout`](crate::Limits),
|
||||||
//! bounds the idle waits).
|
//! bounds the idle waits).
|
||||||
//! - **The typed opens resolve `Err` directly on a server reject**
|
//! - **The typed opens resolve `Err` directly on a server reject**
|
||||||
//! (POC-1 §6) — [`OpError::OpenDeclined`](crate::OpError), family-mirrored
|
//! (POC-1 §6) to [`OpError::OpenDeclined`](crate::OpError), family-mirrored
|
||||||
//! [`OpenDeclineReason`](crate::OpenDeclineReason), **and the
|
//! [`OpenDeclineReason`](crate::OpenDeclineReason), **and the
|
||||||
//! connection stays usable after the decline** (row-pinned per
|
//! connection stays usable after the decline** (row-pinned per
|
||||||
//! decline class).
|
//! decline class).
|
||||||
//! - **No channel-queue consumption where a `into_stream()` conversion
|
//! - **No channel-queue consumption where a `into_stream()` conversion
|
||||||
//! follows** (POC-1 §6's fresh-`wait()` caveat) — the tunnel rows
|
//! follows** (POC-1 §6's fresh-`wait()` caveat): the tunnel rows
|
||||||
//! (the only `into_stream` consumers) go typed-open → straight
|
//! (the only `into_stream` consumers) go typed-open → straight
|
||||||
//! conversion, nothing between. The exit-**consuming** rows
|
//! conversion, nothing between. The exit-**consuming** rows
|
||||||
//! (exec/pty, git) cannot ride `into_stream` at all — the
|
//! (exec/pty, git) cannot ride `into_stream` at all, since the
|
||||||
//! conversion's reader side swallows every non-`Data` message,
|
//! conversion's reader side swallows every non-`Data` message,
|
||||||
//! which is exactly where `ExitStatus`/`ExitSignal` live — so they
|
//! which is exactly where `ExitStatus`/`ExitSignal` live, so they
|
||||||
//! run the **split-halves wait-pump** (`Channel::split()` + a
|
//! run the **split-halves wait-pump** (`Channel::split()` + a
|
||||||
//! spawned task draining `wait()`), the shape the server
|
//! spawned task draining `wait()`), the shape the server
|
||||||
//! direction's git row landed for the same reason
|
//! direction's git row landed for the same reason
|
||||||
//! (`w4-git-exit-close`). The exit-ride is the family plane's (§the
|
//! (`w4-git-exit-close`). The exit-ride is the family plane's (§the
|
||||||
//! session rows) or the consumed-report oneshot's (§the git row).
|
//! session rows) or the consumed-report oneshot's (§the git row).
|
||||||
//! - `window_change`/`signal` are fire-and-forget on the wire (no SSH
|
//! - `window_change`/`signal` are fire-and-forget on the wire (no SSH
|
||||||
//! reply exists) — the pumps send and drop the outcome, never
|
//! reply exists); the pumps send and drop the outcome, never
|
||||||
//! awaiting one.
|
//! awaiting one.
|
||||||
//!
|
//!
|
||||||
//! ## The family plane (the session rows)
|
//! ## The family plane (the session rows)
|
||||||
//!
|
//!
|
||||||
//! The exec/pty session's data plane is the **alktty chunk framing in
|
//! The exec/pty session's data plane is the **alktty chunk framing in
|
||||||
//! both directions** — the exact plane the server direction's session
|
//! both directions**, the exact plane the server direction's session
|
||||||
//! bridge speaks ([`crate::tty_map`]), consumed not re-derived:
|
//! bridge speaks ([`crate::tty_map`]), consumed not re-derived:
|
||||||
//! stdin chunks (`STREAM_STDIN`) ride the wire `data` (the
|
//! stdin chunks (`STREAM_STDIN`) ride the wire `data` (the
|
||||||
//! zero-length sentinel = wire EOF, the bridge's decided translation —
|
//! zero-length sentinel = wire EOF, the bridge's decided translation,
|
||||||
//! the session keeps pumping after it); `CTRL_IN` chunks translate to
|
//! the session keeps pumping after it); `CTRL_IN` chunks translate to
|
||||||
//! the wire request rows (`Resize` → `window_change` (the session
|
//! the wire request rows (`Resize` → `window_change` (the session
|
||||||
//! dimension clamp is not needed — the family plane is already
|
//! dimension clamp is not needed; the family plane is already
|
||||||
//! `u16`), `Signal{name}` → `signal` through the shared table's
|
//! `u16`), `Signal{name}` → `signal` through the shared table's
|
||||||
//! name→`Sig` map, `Eof` → wire EOF); wire `data` → stdout chunks,
|
//! name→`Sig` map, `Eof` → wire EOF); wire `data` → stdout chunks,
|
||||||
//! wire `extended_data(1)` → stderr chunks; and the exit rides
|
//! wire `extended_data(1)` → stderr chunks; and the exit rides
|
||||||
//! **in-band as the family's exit chunk** (`STREAM_CTRL_OUT`,
|
//! **in-band as the family's exit chunk** (`STREAM_CTRL_OUT`,
|
||||||
//! exit-chunk-is-last — alktty's ADR-004, mapping.md's contract):
|
//! exit-chunk-is-last, alktty's ADR-004, mapping.md's contract):
|
||||||
//! wire `ExitStatus(N)` → code `N`, wire `ExitSignal(name)` →
|
//! wire `ExitStatus(N)` → code `N`, wire `ExitSignal(name)` →
|
||||||
//! `−signum` through the shared table's inverse read
|
//! `−signum` through the shared table's inverse read
|
||||||
//! ([`crate::exit_code`] — the derivation contract's client arm; a
|
//! ([`crate::exit_code`], the derivation contract's client arm; a
|
||||||
//! name with no table entry rides the `−1` sentinel, the mapping
|
//! name with no table entry rides the `−1` sentinel, the mapping
|
||||||
//! never invents). A session that ends with **no exit report**
|
//! never invents). A session that ends with **no exit report**
|
||||||
//! delivers no exit chunk — the output half just ends (mapping.md's
|
//! delivers no exit chunk; the output half just ends (mapping.md's
|
||||||
//! no-exit-chunk close family: "reports nothing"). The session's
|
//! no-exit-chunk close family: "reports nothing"). The session's
|
||||||
//! ttymodes never ride the wire pty request (the OQ-44
|
//! ttymodes never ride the wire pty request (the OQ-44
|
||||||
//! reserved-JSON posture mirrored: the door never invents wire
|
//! reserved-JSON posture mirrored: the door never invents wire
|
||||||
@@ -78,30 +78,30 @@
|
|||||||
//! | tunnel target (`-L`; the alksocks `DialFn`) | `tunnel` | `channel_open_direct_tcpip` | [`SshConnection::tunnel_direct`] |
|
//! | tunnel target (`-L`; the alksocks `DialFn`) | `tunnel` | `channel_open_direct_tcpip` | [`SshConnection::tunnel_direct`] |
|
||||||
//! | the streamlocal twin | `tunnel` | `channel_open_direct_streamlocal`, runtime-Unix-gated | [`SshConnection::tunnel_direct_unix`] |
|
//! | the streamlocal twin | `tunnel` | `channel_open_direct_streamlocal`, runtime-Unix-gated | [`SshConnection::tunnel_direct_unix`] |
|
||||||
//! | upstream git service | `git` | the shared grammar's command + `exec`; the pack duplex | [`SshConnection::git_service`] |
|
//! | upstream git service | `git` | the shared grammar's command + `exec`; the pack duplex | [`SshConnection::git_service`] |
|
||||||
//! | sftp | *every build* | **none** — the OQ-13 decline placeholder | [`SshConnection::sftp`] |
|
//! | sftp | *every build* | **none**, the OQ-13 decline placeholder | [`SshConnection::sftp`] |
|
||||||
//!
|
//!
|
||||||
//! The exec-command **length budget** applies identically to the
|
//! The exec-command **length budget** applies identically to the
|
||||||
//! command-carrying rows (the server direction's pre-parse cap
|
//! command-carrying rows (the server direction's pre-parse cap
|
||||||
//! mirrored at the command-construction gate — bounded resources;
|
//! mirrored at the command-construction gate; bounded resources,
|
||||||
//! an unbounded command buffer can never ride a dial):
|
//! an unbounded command buffer can never ride a dial):
|
||||||
//! [`OpError::CommandTooLong`](crate::OpError) before any wire op.
|
//! [`OpError::CommandTooLong`](crate::OpError) before any wire op.
|
||||||
//!
|
//!
|
||||||
//! No client-side in-crate channel cap *admits* ops (the server
|
//! No client-side in-crate channel cap *admits* ops (the server
|
||||||
//! direction's cap is a wire-battery defense; the client rows are
|
//! direction's cap is a wire-battery defense; the client rows are
|
||||||
//! hub-driven — the family app chose the dial): bounded resources on
|
//! hub-driven (the family app chose the dial): bounded resources on
|
||||||
//! this direction are the config's always-on wall clock/keepalives,
|
//! this direction are the config's always-on wall clock/keepalives,
|
||||||
//! the russh windows, the bounded duplexes/mpscs the pumps ride, and
|
//! the russh windows, the bounded duplexes/mpscs the pumps ride, and
|
||||||
//! the drain cap — every spawn bounded by its own task's end
|
//! the drain cap; every spawn bounded by its own task's end
|
||||||
//! conditions.
|
//! conditions.
|
||||||
//!
|
//!
|
||||||
//! ## Spawn posture
|
//! ## Spawn posture
|
||||||
//!
|
//!
|
||||||
//! The op methods await their launch (typed open, requests, the
|
//! The op methods await their launch (typed open, requests, the
|
||||||
//! reply drains) on the *caller's* task — never the russh packet
|
//! reply drains) on the *caller's* task, never the russh packet
|
||||||
//! loop's — so the server direction's spawn-and-return deadlock class
|
//! loop's, so the server direction's spawn-and-return deadlock class
|
||||||
//! does not exist here; the long-lived pumps spawn at launch and end
|
//! does not exist here; the long-lived pumps spawn at launch and end
|
||||||
//! on their terminal conditions (the exit chunk, the channel's end,
|
//! on their terminal conditions (the exit chunk, the channel's end,
|
||||||
//! or the hub dropping the surface — a drop is wire-clean: the input
|
//! or the hub dropping the surface; a drop is wire-clean: the input
|
||||||
//! pump's terminal path closes the channel, the pump pair reaping as
|
//! pump's terminal path closes the channel, the pump pair reaping as
|
||||||
//! their duplex halves drop).
|
//! their duplex halves drop).
|
||||||
|
|
||||||
@@ -117,13 +117,13 @@ use crate::client::SshConnection;
|
|||||||
use crate::client_handler::ClientHandler;
|
use crate::client_handler::ClientHandler;
|
||||||
use crate::error::OpError;
|
use crate::error::OpError;
|
||||||
|
|
||||||
/// The op-row pump duplex capacity — the crate's bridge-pair
|
/// The op-row pump duplex capacity, the crate's bridge-pair
|
||||||
/// convention (the wave-2 loopback harness's per-direction capacity),
|
/// convention (the wave-2 loopback harness's per-direction capacity),
|
||||||
/// keeping every pump's backpressure bounded.
|
/// keeping every pump's backpressure bounded.
|
||||||
#[cfg(any(feature = "session", feature = "git"))]
|
#[cfg(any(feature = "session", feature = "git"))]
|
||||||
const OP_DUPLEX_CAPACITY: usize = 65_536;
|
const OP_DUPLEX_CAPACITY: usize = 65_536;
|
||||||
|
|
||||||
/// The pump-side read buffer — the SSH-side granularity (per-chunk
|
/// The pump-side read buffer, the SSH-side granularity (per-chunk
|
||||||
/// payloads are bounded by the dial's `maximum_packet_size`).
|
/// payloads are bounded by the dial's `maximum_packet_size`).
|
||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
const PUMP_BUF: usize = 16 * 1024;
|
const PUMP_BUF: usize = 16 * 1024;
|
||||||
@@ -137,14 +137,14 @@ const DRAIN_MESSAGE_CAP: usize = 1024;
|
|||||||
/// The originator label the door puts on the wire's `direct-tcpip`
|
/// The originator label the door puts on the wire's `direct-tcpip`
|
||||||
/// opens (the SSH originator fields are what a vanilla client fills
|
/// opens (the SSH originator fields are what a vanilla client fills
|
||||||
/// in for its own local listener; this door's hub-driven dials carry
|
/// in for its own local listener; this door's hub-driven dials carry
|
||||||
/// the door's label and no port — recorded decision: nothing
|
/// the door's label and no port, recorded decision: nothing
|
||||||
/// invented, nothing leaked, the peer's logs name the door).
|
/// invented, nothing leaked, the peer's logs name the door).
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
const TUNNEL_ORIGINATOR: &str = "alk-ssh";
|
const TUNNEL_ORIGINATOR: &str = "alk-ssh";
|
||||||
|
|
||||||
/// The wire exit status → the family `i32` exit-code convention: a
|
/// The wire exit status → the family `i32` exit-code convention: a
|
||||||
/// code beyond the family `i32` range is unwritable as an exit-chunk
|
/// code beyond the family `i32` range is unwritable as an exit-chunk
|
||||||
/// code; the clamp rides (defensive corner — vanilla command codes
|
/// code; the clamp rides (defensive corner, vanilla command codes
|
||||||
/// are far below it; the mapping never invents a value, it narrows).
|
/// are far below it; the mapping never invents a value, it narrows).
|
||||||
#[cfg(any(feature = "session", feature = "git"))]
|
#[cfg(any(feature = "session", feature = "git"))]
|
||||||
fn family_exit_code(status: u32) -> i32 {
|
fn family_exit_code(status: u32) -> i32 {
|
||||||
@@ -155,7 +155,7 @@ fn family_exit_code(status: u32) -> i32 {
|
|||||||
/// op taxonomy (a channel-open failure →
|
/// op taxonomy (a channel-open failure →
|
||||||
/// [`OpError::OpenDeclined`](crate::OpError::OpenDeclined),
|
/// [`OpError::OpenDeclined`](crate::OpError::OpenDeclined),
|
||||||
/// family-mirrored; everything else transparent). The russh level's
|
/// family-mirrored; everything else transparent). The russh level's
|
||||||
/// test observable (`POC-1 §6`'s `Err(ChannelOpenFailure)` — the
|
/// test observable (`POC-1 §6`'s `Err(ChannelOpenFailure)`, the
|
||||||
/// awaiting typed call's resolution) rides this point; the
|
/// awaiting typed call's resolution) rides this point; the
|
||||||
/// connection stays usable after every decline (row-pinned).
|
/// connection stays usable after every decline (row-pinned).
|
||||||
#[cfg(any(feature = "session", feature = "git", feature = "tunnel"))]
|
#[cfg(any(feature = "session", feature = "git", feature = "tunnel"))]
|
||||||
@@ -165,7 +165,7 @@ fn open_error(error: russh::Error) -> OpError {
|
|||||||
|
|
||||||
/// The request-reply drain (the §module docs' recorded mechanics,
|
/// The request-reply drain (the §module docs' recorded mechanics,
|
||||||
/// shared by the exec/pty rows and the git row): Success/Failure via
|
/// shared by the exec/pty rows and the git row): Success/Failure via
|
||||||
/// `wait()` — never an `Err` from the request call — `continue` past
|
/// `wait()`, never an `Err` from the request call; `continue` past
|
||||||
/// the interleaved rest (the POC-3 §D.3 break-on-first-message trap,
|
/// the interleaved rest (the POC-3 §D.3 break-on-first-message trap,
|
||||||
/// not repeated), message-capped. `Failure` is the wire
|
/// not repeated), message-capped. `Failure` is the wire
|
||||||
/// request-refusal class ([`OpError::RequestRefused`]); the
|
/// request-refusal class ([`OpError::RequestRefused`]); the
|
||||||
@@ -197,28 +197,28 @@ async fn drain_launch_request(channel: &mut russh::Channel<Msg>) -> Result<(), O
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// The sftp placeholder — every build (OQ-13; no feature, no dep)
|
// The sftp placeholder, every build (OQ-13; no feature, no dep)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
impl SshConnection {
|
impl SshConnection {
|
||||||
/// The sftp op — the client-side half of the OQ-13 decline
|
/// The sftp op, the client-side half of the OQ-13 decline
|
||||||
/// placeholder: **declines in every build, before any wire op**
|
/// placeholder: **declines in every build, before any wire op**
|
||||||
/// (never a half-opened subsystem channel). The feature exists
|
/// (never a half-opened subsystem channel). The feature exists
|
||||||
/// and ships in the build string; the route does not, and arrives
|
/// and ships in the build string; the route does not, and arrives
|
||||||
/// with alkfs as its own ADR — both directions' placeholders are
|
/// with alkfs as its own ADR; both directions' placeholders are
|
||||||
/// then replaced by real rows (`client.md` §What the crate does
|
/// then replaced by real rows (`client.md` §What the crate does
|
||||||
/// *not* include). Compiles feature-free; the `Ok` type is
|
/// *not* include). Compiles feature-free; the `Ok` type is
|
||||||
/// `Infallible` by design — nothing can ever populate it here.
|
/// `Infallible` by design; nothing can ever populate it here.
|
||||||
pub async fn sftp(&self) -> Result<std::convert::Infallible, OpError> {
|
pub async fn sftp(&self) -> Result<std::convert::Infallible, OpError> {
|
||||||
Err(OpError::SftpNotServed)
|
Err(OpError::SftpNotServed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// The session rows — exec / pty (under `session`)
|
// The session rows: exec / pty (under `session`)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// A remote **session** — the family session plane over the runner
|
/// A remote **session**: the family session plane over the runner
|
||||||
/// (the session rows' surface): the alktty chunk framing in both
|
/// (the session rows' surface): the alktty chunk framing in both
|
||||||
/// directions, exactly the plane the server direction's session
|
/// directions, exactly the plane the server direction's session
|
||||||
/// bridge speaks (`tty_map`), reversed. Two duplex halves
|
/// bridge speaks (`tty_map`), reversed. Two duplex halves
|
||||||
@@ -245,7 +245,7 @@ impl std::fmt::Debug for RemoteSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which request launches a session — the family op's payload
|
/// Which request launches a session: the family op's payload
|
||||||
/// (§the row inventory; pipe-mode exec vs the shell form).
|
/// (§the row inventory; pipe-mode exec vs the shell form).
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
enum LaunchRequest {
|
enum LaunchRequest {
|
||||||
@@ -255,11 +255,11 @@ enum LaunchRequest {
|
|||||||
|
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
impl SshConnection {
|
impl SshConnection {
|
||||||
/// The pipe-mode exec row: `ssh host "cmd"` in, family-typed —
|
/// The pipe-mode exec row: `ssh host "cmd"` in, family-typed;
|
||||||
/// the session channel's open + the wire `exec` request, then the
|
/// the session channel's open + the wire `exec` request, then the
|
||||||
/// chunk-framed session (§the family plane; stderr separated).
|
/// chunk-framed session (§the family plane; stderr separated).
|
||||||
/// The command rides the wire verbatim (the remote end's shell
|
/// The command rides the wire verbatim (the remote end's shell
|
||||||
/// parses it — the vanilla exec grammar; never argv-split).
|
/// parses it, the vanilla exec grammar; never argv-split).
|
||||||
pub async fn exec(&self, command: impl Into<String>) -> Result<RemoteSession, OpError> {
|
pub async fn exec(&self, command: impl Into<String>) -> Result<RemoteSession, OpError> {
|
||||||
let command = command.into();
|
let command = command.into();
|
||||||
if command.len() > self.budgets().max_exec_command_len {
|
if command.len() > self.budgets().max_exec_command_len {
|
||||||
@@ -272,7 +272,7 @@ impl SshConnection {
|
|||||||
/// carries the family
|
/// carries the family
|
||||||
/// [`alktty::negotiation::TerminalParamsWire`](alktty::negotiation::
|
/// [`alktty::negotiation::TerminalParamsWire`](alktty::negotiation::
|
||||||
/// TerminalParamsWire) first (its acceptance drained), then the
|
/// TerminalParamsWire) first (its acceptance drained), then the
|
||||||
/// exec. **A refused pty refuses the whole row** — never a
|
/// exec. **A refused pty refuses the whole row**: never a
|
||||||
/// half-capability, never a silent pty-less fall.
|
/// half-capability, never a silent pty-less fall.
|
||||||
pub async fn pty_exec(
|
pub async fn pty_exec(
|
||||||
&self,
|
&self,
|
||||||
@@ -292,8 +292,8 @@ impl SshConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The shell row ("own a shell on the runner"): with
|
/// The shell row ("own a shell on the runner"): with
|
||||||
/// `params` — the pty mode (the interactive `ssh` shape); with
|
/// `params`, this is the pty mode (the interactive `ssh` shape); with
|
||||||
/// `None` — the pipe-mode shell (the `ssh -T` shape; a pty-less
|
/// `None`, the pipe-mode shell (the `ssh -T` shape; a pty-less
|
||||||
/// shell request is legitimate vanilla and served, never declined,
|
/// shell request is legitimate vanilla and served, never declined,
|
||||||
/// mirroring the server direction's decided parity).
|
/// mirroring the server direction's decided parity).
|
||||||
pub async fn shell(
|
pub async fn shell(
|
||||||
@@ -361,7 +361,7 @@ async fn open_remote_session(
|
|||||||
/// `CTRL_IN` `Eof` / the hub's input half dropping = the wire EOF
|
/// `CTRL_IN` `Eof` / the hub's input half dropping = the wire EOF
|
||||||
/// *once*, then the idle-for-kill state (the session keeps pumping);
|
/// *once*, then the idle-for-kill state (the session keeps pumping);
|
||||||
/// the terminal kill (from the output pump's terminal paths) → the
|
/// the terminal kill (from the output pump's terminal paths) → the
|
||||||
/// wire `channel_close` — the session's drop is wire-clean.
|
/// wire `channel_close`; the session's drop is wire-clean.
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
async fn session_input_pump(
|
async fn session_input_pump(
|
||||||
write_half: russh::ChannelWriteHalf<Msg>,
|
write_half: russh::ChannelWriteHalf<Msg>,
|
||||||
@@ -432,12 +432,12 @@ async fn session_input_pump(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One frame read from the hub→door plane — the *no-disambiguation*
|
/// One frame read from the hub→door plane, in the *no-disambiguation*
|
||||||
/// read direction: this plane's chunks are the family protocol's own
|
/// read direction: this plane's chunks are the family protocol's own
|
||||||
/// (stdin `0` / `CTRL_IN` `3`); no negotiation-error frames exist
|
/// (stdin `0` / `CTRL_IN` `3`); no negotiation-error frames exist
|
||||||
/// toward the door, so the read is the protocol's direct
|
/// toward the door, so the read is the protocol's direct
|
||||||
/// [`ChunkReader::read_chunk`] (a `0x00`-leading frame here is a
|
/// [`ChunkReader::read_chunk`] (a `0x00`-leading frame here is a
|
||||||
/// zero-length stdin chunk — the sentinel — and reads as exactly
|
/// zero-length stdin chunk (the sentinel) and reads as exactly
|
||||||
/// that). A rogue frame is the bounds-checked [`RawError`], mapped to
|
/// that). A rogue frame is the bounds-checked [`RawError`], mapped to
|
||||||
/// the pump's [`HubFrame::Break`].
|
/// the pump's [`HubFrame::Break`].
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
@@ -459,7 +459,7 @@ async fn hub_frame(reader: &mut alktty::wire::ChunkReader<tokio::io::DuplexStrea
|
|||||||
/// The session row's **output pump**: the wire → the hub's chunk
|
/// The session row's **output pump**: the wire → the hub's chunk
|
||||||
/// plane (§the family plane). `data` → stdout chunks,
|
/// plane (§the family plane). `data` → stdout chunks,
|
||||||
/// `extended_data(1)` → stderr chunks, and the `ExitStatus` /
|
/// `extended_data(1)` → stderr chunks, and the `ExitStatus` /
|
||||||
/// `ExitSignal` terminal → the in-band exit chunk (last) — then the
|
/// `ExitSignal` terminal → the in-band exit chunk (last), then the
|
||||||
/// kill fires and the input pump closes the wire. A channel end with
|
/// kill fires and the input pump closes the wire. A channel end with
|
||||||
/// no report → no exit chunk; the output half just ends.
|
/// no report → no exit chunk; the output half just ends.
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
@@ -512,10 +512,10 @@ async fn session_output_pump(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The family exit chunk's write — `{"type":"exit","code":N}` as the
|
/// The family exit chunk's write: `{"type":"exit","code":N}` as the
|
||||||
/// in-band terminal chunk (exit-chunk-is-last), serialized by the
|
/// in-band terminal chunk (exit-chunk-is-last), serialized by the
|
||||||
/// family currency's own serializer. An unserializable exit (not a
|
/// family currency's own serializer. An unserializable exit (not a
|
||||||
/// reachable shape) writes nothing — the no-report family, never a
|
/// reachable shape) writes nothing; the no-report family, never a
|
||||||
/// corrupted frame.
|
/// corrupted frame.
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
async fn write_exit_chunk(
|
async fn write_exit_chunk(
|
||||||
@@ -533,16 +533,16 @@ async fn write_exit_chunk(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// The tunnel rows — direct-tcpip / direct-streamlocal (under `tunnel`)
|
// The tunnel rows: direct-tcpip / direct-streamlocal (under `tunnel`)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// A tunnel channel — the tunnel rows' surface: a plain byte duplex
|
/// A tunnel channel, the tunnel rows' surface: a plain byte duplex
|
||||||
/// over the open channel (`-L`'s pump; the alksocks `DialFn` composes
|
/// over the open channel (`-L`'s pump; the alksocks `DialFn` composes
|
||||||
/// exactly this type). Nothing vanilla-typed rides the family
|
/// exactly this type). Nothing vanilla-typed rides the family
|
||||||
/// surface — the duplex's `poll_shutdown` carries the wire EOF.
|
/// surface; the duplex's `poll_shutdown` carries the wire EOF.
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
pub struct TunnelChannel {
|
pub struct TunnelChannel {
|
||||||
/// The russh channel's byte duplex (`pub(crate)` — the `-R` leg's
|
/// The russh channel's byte duplex (`pub(crate)`; the `-R` leg's
|
||||||
/// acceptor payloads construct the type over a server-initiated
|
/// acceptor payloads construct the type over a server-initiated
|
||||||
/// open's `into_stream()` conversion; downstream crates see the
|
/// open's `into_stream()` conversion; downstream crates see the
|
||||||
/// type opaque as ever).
|
/// type opaque as ever).
|
||||||
@@ -595,11 +595,11 @@ impl tokio::io::AsyncWrite for TunnelChannel {
|
|||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
impl SshConnection {
|
impl SshConnection {
|
||||||
/// The tunnel-target row (`-L`; the alksocks `DialFn` composes
|
/// The tunnel-target row (`-L`; the alksocks `DialFn` composes
|
||||||
/// over it): the typed open — server reject →
|
/// over it): the typed open; server reject →
|
||||||
/// [`OpError::OpenDeclined`](crate::OpError) with the mirrored
|
/// [`OpError::OpenDeclined`](crate::OpError) with the mirrored
|
||||||
/// class, and the connection stays usable (row-pinned per
|
/// class, and the connection stays usable (row-pinned per
|
||||||
/// decline class). Between the open and the `into_stream()`
|
/// decline class). Between the open and the `into_stream()`
|
||||||
/// conversion **nothing else happens** — no channel-queue
|
/// conversion **nothing else happens**: no channel-queue
|
||||||
/// consumption (POC-1 §6's caveat, respected by construction; the
|
/// consumption (POC-1 §6's caveat, respected by construction; the
|
||||||
/// data pump is the raw byte duplex).
|
/// data pump is the raw byte duplex).
|
||||||
pub async fn tunnel_direct(
|
pub async fn tunnel_direct(
|
||||||
@@ -618,7 +618,7 @@ impl SshConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The streamlocal twin (`direct-streamlocal@openssh.com`): same
|
/// The streamlocal twin (`direct-streamlocal@openssh.com`): same
|
||||||
/// typed-open/pump shape, the OQ-11 runtime-Unix gate mirrored —
|
/// typed-open/pump shape, the OQ-11 runtime-Unix gate mirrored;
|
||||||
/// **compiles wherever `tunnel` compiles**, and off-Unix the op
|
/// **compiles wherever `tunnel` compiles**, and off-Unix the op
|
||||||
/// declines cleanly ([`OpError::UnixUnavailable`](crate::OpError))
|
/// declines cleanly ([`OpError::UnixUnavailable`](crate::OpError))
|
||||||
/// before any wire op.
|
/// before any wire op.
|
||||||
@@ -646,15 +646,15 @@ impl SshConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// The git row — upstream exec consumption (under `git`)
|
// The git row: upstream exec consumption (under `git`)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// An upstream **git service channel** — the git row's surface: the
|
/// An upstream **git service channel**, the git row's surface: the
|
||||||
/// raw pack duplex (the git protocol's own byte plane rides the
|
/// raw pack duplex (the git protocol's own byte plane rides the
|
||||||
/// channel untouched), the sshd-side stderr split out, and the
|
/// channel untouched), the sshd-side stderr split out, and the
|
||||||
/// consumed exit report at the family's `i32` exit-code convention
|
/// consumed exit report at the family's `i32` exit-code convention
|
||||||
/// (`ExitStatus(N)` → `N`; `ExitSignal` → `−signum` through the
|
/// (`ExitStatus(N)` → `N`; `ExitSignal` → `−signum` through the
|
||||||
/// shared table — the mapping never invents; a receive `Err` on
|
/// shared table, the mapping never invents; a receive `Err` on
|
||||||
/// `exit` = the pump ended with **no exit report at all**, the
|
/// `exit` = the pump ended with **no exit report at all**, the
|
||||||
/// no-exit-chunk close family's mirror).
|
/// no-exit-chunk close family's mirror).
|
||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
@@ -666,7 +666,7 @@ pub struct GitPackSession {
|
|||||||
/// the pack).
|
/// the pack).
|
||||||
pub output: tokio::io::DuplexStream,
|
pub output: tokio::io::DuplexStream,
|
||||||
/// The wire `extended_data(1)` (the remote program's stderr
|
/// The wire `extended_data(1)` (the remote program's stderr
|
||||||
/// diagnostics) — raw bytes; a hub uninterested in diagnostics
|
/// diagnostics), raw bytes; a hub uninterested in diagnostics
|
||||||
/// drops the half (the pump then discards further stderr).
|
/// drops the half (the pump then discards further stderr).
|
||||||
pub stderr: tokio::io::DuplexStream,
|
pub stderr: tokio::io::DuplexStream,
|
||||||
/// The consumed exit report (see the type docs).
|
/// The consumed exit report (see the type docs).
|
||||||
@@ -683,7 +683,7 @@ impl std::fmt::Debug for GitPackSession {
|
|||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
impl SshConnection {
|
impl SshConnection {
|
||||||
/// The upstream git row: the shared exec grammar's command
|
/// The upstream git row: the shared exec grammar's command
|
||||||
/// (`git-{upload,receive}-pack '/<repo>'` — built through
|
/// (`git-{upload,receive}-pack '/<repo>'`, built through
|
||||||
/// `crate::dispatch`'s construction arm, the parse arm's exact
|
/// `crate::dispatch`'s construction arm, the parse arm's exact
|
||||||
/// inverse; no forked grammar), pointed at the vanilla `sshd`:
|
/// inverse; no forked grammar), pointed at the vanilla `sshd`:
|
||||||
/// typed open + `exec`, the pack duplex pumped, the exit report
|
/// typed open + `exec`, the pack duplex pumped, the exit report
|
||||||
@@ -695,9 +695,9 @@ impl SshConnection {
|
|||||||
/// [`OpError::InvalidRepoId`](crate::OpError);
|
/// [`OpError::InvalidRepoId`](crate::OpError);
|
||||||
/// the constructed command is checked against the exec-length
|
/// the constructed command is checked against the exec-length
|
||||||
/// budget ([`crate::Limits::max_exec_command_len`]) →
|
/// budget ([`crate::Limits::max_exec_command_len`]) →
|
||||||
/// [`OpError::CommandTooLong`](crate::OpError) — both **before any
|
/// [`OpError::CommandTooLong`](crate::OpError), both **before any
|
||||||
/// wire op**. (The grammar's other decline classes —
|
/// wire op**. (The grammar's other decline classes,
|
||||||
/// `unknown-service` / `service-only` — are structurally
|
/// `unknown-service` / `service-only`, are structurally
|
||||||
/// unreachable here: the [`alkgit::Service`] input type carries
|
/// unreachable here: the [`alkgit::Service`] input type carries
|
||||||
/// only the two serving services; the construction arm can emit
|
/// only the two serving services; the construction arm can emit
|
||||||
/// no other token.)
|
/// no other token.)
|
||||||
@@ -793,7 +793,7 @@ async fn git_input_pump(
|
|||||||
|
|
||||||
/// The git row's **output pump**: the wire → the pack duplex +
|
/// The git row's **output pump**: the wire → the pack duplex +
|
||||||
/// stderr; the exit report consumed here (the family code) and the
|
/// stderr; the exit report consumed here (the family code) and the
|
||||||
/// kill fired — then the input pump closes the wire. A channel end
|
/// kill fired, then the input pump closes the wire. A channel end
|
||||||
/// with no report → no exit fired; the `exit` oneshot drops (the
|
/// with no report → no exit fired; the `exit` oneshot drops (the
|
||||||
/// hub's receive reads the no-report mirror).
|
/// hub's receive reads the no-report mirror).
|
||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
@@ -848,7 +848,7 @@ mod tests {
|
|||||||
use russh::ChannelOpenFailure;
|
use russh::ChannelOpenFailure;
|
||||||
|
|
||||||
/// - *Contract stamp:* `client.md` §op rows (no vanilla-concept
|
/// - *Contract stamp:* `client.md` §op rows (no vanilla-concept
|
||||||
/// leaks up) + POC-1 §6 — the typed open's decline mapping
|
/// leaks up) + POC-1 §6: the typed open's decline mapping
|
||||||
/// preserves the wire class (`error.rs`'s mirror rows; this
|
/// preserves the wire class (`error.rs`'s mirror rows; this
|
||||||
/// pins the op rows' own consumption point).
|
/// pins the op rows' own consumption point).
|
||||||
#[test]
|
#[test]
|
||||||
@@ -870,7 +870,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the family exit-currency clamp (§the
|
/// - *Contract stamp:* the family exit-currency clamp (§the
|
||||||
/// family plane) — ordinary codes ride identity, beyond-range
|
/// family plane); ordinary codes ride identity, beyond-range
|
||||||
/// ones narrow (never invent).
|
/// ones narrow (never invent).
|
||||||
#[cfg(any(feature = "session", feature = "git"))]
|
#[cfg(any(feature = "session", feature = "git"))]
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+82
-82
@@ -1,20 +1,20 @@
|
|||||||
//! The pure configuration surface — the shapes every later wave's
|
//! The pure configuration surface: the shapes every later wave's
|
||||||
//! constructors take (serve-config, dial-config, [`Limits`]). Types
|
//! constructors take (serve-config, dial-config, [`Limits`]). Types
|
||||||
//! only: russh `Config` construction is wave-2 material, dialing lands
|
//! only: russh `Config` construction is wave-2 material, dialing lands
|
||||||
//! at wave 4; nothing here touches the wire or the vault.
|
//! at wave 4; nothing here touches the wire or the vault.
|
||||||
//!
|
//!
|
||||||
//! Two directions, one budget model (`overview.md` §Public surface):
|
//! Two directions, one budget model (`overview.md` §Public surface):
|
||||||
//!
|
//!
|
||||||
//! - [`ServeConfig`] — the server direction's assembly-supplied serving
|
//! - [`ServeConfig`]: the server direction's assembly-supplied serving
|
||||||
//! configuration: the OQ-08 fields ([`ServeConfig::shell_argv`] /
|
//! configuration: the OQ-08 fields ([`ServeConfig::shell_argv`] /
|
||||||
//! [`ServeConfig::exec_wrapper_argv`]), the injected identity,
|
//! [`ServeConfig::exec_wrapper_argv`]), the injected identity,
|
||||||
//! host-key and tunnel handles, the per-feature family-seam handles
|
//! host-key and tunnel handles, the per-feature family-seam handles
|
||||||
//! (the wave-3 extension — [`SessionSeam`] carries the alktty
|
//! (the wave-3 extension, where [`SessionSeam`] carries the alktty
|
||||||
//! backends map + backend selector + ownership provider;
|
//! backends map + backend selector + ownership provider;
|
||||||
//! [`GitSeam`] carries the alkgit backend quartet + the caller-owned
|
//! [`GitSeam`] carries the alkgit backend quartet + the caller-owned
|
||||||
//! pipeline + the push-options flag), the OQ-09 `cwd`/`env` values,
|
//! pipeline + the push-options flag), the OQ-09 `cwd`/`env` values,
|
||||||
//! and the [`Limits`].
|
//! and the [`Limits`].
|
||||||
//! - [`DialConfig`] — the client direction's dial configuration
|
//! - [`DialConfig`]: the client direction's dial configuration
|
||||||
//! (`client.md` §The dial surface): address, `username`, key source,
|
//! (`client.md` §The dial surface): address, `username`, key source,
|
||||||
//! host-key policy, and the bounded-resource knobs (carried by
|
//! host-key policy, and the bounded-resource knobs (carried by
|
||||||
//! [`Limits`]).
|
//! [`Limits`]).
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
//! Crate rules this module encodes:
|
//! Crate rules this module encodes:
|
||||||
//!
|
//!
|
||||||
//! - **Unset-declines, no crate defaults** (OQ-08): the OQ-08 fields
|
//! - **Unset-declines, no crate defaults** (OQ-08): the OQ-08 fields
|
||||||
//! are `Option` and their unset semantics live in the type shape —
|
//! are `Option` and their unset semantics live in the type shape,
|
||||||
//! an unset field makes its row decline with a clean error at
|
//! an unset field makes its row decline with a clean error at
|
||||||
//! request time (wave-2/3 consumption), never a fallback, never
|
//! request time (wave-2/3 consumption), never a fallback, never
|
||||||
//! construction-time validation (`error.rs`'s no-config-error
|
//! construction-time validation (`error.rs`'s no-config-error
|
||||||
@@ -33,10 +33,10 @@
|
|||||||
//! [`ServeConfig::git_seam`] unset → the git row declines even
|
//! [`ServeConfig::git_seam`] unset → the git row declines even
|
||||||
//! though `git` is on (the decline matrix's config-driven notes;
|
//! though `git` is on (the decline matrix's config-driven notes;
|
||||||
//! the feature alone configures nothing). The tunnel branch carries
|
//! the feature alone configures nothing). The tunnel branch carries
|
||||||
//! its flat `Option<Arc<ChannelClient>>` — an egress-only
|
//! its flat `Option<Arc<ChannelClient>>`, because an egress-only
|
||||||
//! (route-1-only) door needs no struct for leg 1 — and the `-R`
|
//! (route-1-only) door needs no struct for leg 1, and the `-R`
|
||||||
//! listen leg's registration seam landed with `w3-tunnel-listen`
|
//! listen leg's registration seam landed with `w3-tunnel-listen`
|
||||||
//! (`ServeConfig::tunnel_listen_seam` — the wave-3 seam-config's
|
//! (`ServeConfig::tunnel_listen_seam`, the wave-3 seam-config's
|
||||||
//! second course-correction; the shape decision recorded on
|
//! second course-correction; the shape decision recorded on
|
||||||
//! [`TunnelListenSeam`]: unset → the listen rows decline even with
|
//! [`TunnelListenSeam`]: unset → the listen rows decline even with
|
||||||
//! a fabric leg present, because the door-side inbound-op serving
|
//! a fabric leg present, because the door-side inbound-op serving
|
||||||
@@ -48,17 +48,17 @@
|
|||||||
//! no vault lifecycle of its own. A git-only tunnel build simply
|
//! no vault lifecycle of its own. A git-only tunnel build simply
|
||||||
//! leaves [`ServeConfig::channel_client`] absent.
|
//! leaves [`ServeConfig::channel_client`] absent.
|
||||||
//! - **Bounded resources everywhere** (convention 9): every session
|
//! - **Bounded resources everywhere** (convention 9): every session
|
||||||
//! — both directions — carries a [`Limits`]; there is no
|
//! in both directions carries a [`Limits`]; there is no
|
||||||
//! unbounded-loop or unbounded-buffer configuration shape the
|
//! unbounded-loop or unbounded-buffer configuration shape the
|
||||||
//! assembly can build from these types.
|
//! assembly can build from these types.
|
||||||
//!
|
//!
|
||||||
//! Knob-name parity with the russh 0.65 pin (ADR-002):
|
//! Knob-name parity with the russh 0.65 pin (ADR-002):
|
||||||
//! `window_size`, `maximum_packet_size`, `channel_buffer_size`,
|
//! `window_size`, `maximum_packet_size`, `channel_buffer_size`,
|
||||||
//! `inactivity_timeout`, `keepalive_interval`, `keepalive_max`,
|
//! `inactivity_timeout`, `keepalive_interval`, `keepalive_max`,
|
||||||
//! `max_auth_attempts` — wave-2/wave-4 map these fields into the russh
|
//! `max_auth_attempts`; wave-2/wave-4 map these fields into the russh
|
||||||
//! `Config` verbatim. russh's rekey `Limits` stay russh-enforced at
|
//! `Config` verbatim. russh's rekey `Limits` stay russh-enforced at
|
||||||
//! its own RFC-4253 §9 defaults (wave-2); they are not carried here —
|
//! its own RFC-4253 §9 defaults (wave-2); they are not carried here,
|
||||||
//! this module's [`Limits`] is the crate's in-crate + mapped-knob
|
//! since this module's [`Limits`] is the crate's in-crate + mapped-knob
|
||||||
//! budget model.
|
//! budget model.
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -84,15 +84,15 @@ use crate::key_source::{HostKeySource, KeySource};
|
|||||||
|
|
||||||
/// The bounded-resources budget model (`overview.md`
|
/// The bounded-resources budget model (`overview.md`
|
||||||
/// §Bounded-resources policy): the in-crate caps plus the russh-knob
|
/// §Bounded-resources policy): the in-crate caps plus the russh-knob
|
||||||
/// mapping with the documented default postures. Every session —
|
/// mapping with the documented default postures. Every session,
|
||||||
/// server and client direction — carries one (`AGENTS.md` convention
|
/// server or client direction, carries one (`AGENTS.md` convention
|
||||||
/// 9); there is no unbounded configuration shape.
|
/// 9); there is no unbounded configuration shape.
|
||||||
///
|
///
|
||||||
/// Carried by value (`Copy`): a [`Limits`] is plain tuning data, and
|
/// Carried by value (`Copy`): a [`Limits`] is plain tuning data, and
|
||||||
/// wave-2/4 copies it into the russh `Config` construction.
|
/// wave-2/4 copies it into the russh `Config` construction.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct Limits {
|
pub struct Limits {
|
||||||
/// The crates' own cap on concurrent channels per session —
|
/// The crates' own cap on concurrent channels per session;
|
||||||
/// opened channels beyond the cap are declined (fail-closed
|
/// opened channels beyond the cap are declined (fail-closed
|
||||||
/// refuse, never queue). The per-identity policy layer (when the
|
/// refuse, never queue). The per-identity policy layer (when the
|
||||||
/// alkcall bridge carries it) is additional and comes with the
|
/// alkcall bridge carries it) is additional and comes with the
|
||||||
@@ -102,39 +102,39 @@ pub struct Limits {
|
|||||||
pub max_concurrent_channels: usize,
|
pub max_concurrent_channels: usize,
|
||||||
/// The parse-input cap for the wave-3 exec grammar: a wire exec
|
/// The parse-input cap for the wave-3 exec grammar: a wire exec
|
||||||
/// command string longer than this declines before any parse or
|
/// command string longer than this declines before any parse or
|
||||||
/// resource allocation (decline-first, alloc-never — the ADR-006
|
/// resource allocation (decline-first, alloc-never, the ADR-006
|
||||||
/// dispatch contract). Assembly-tunable.
|
/// dispatch contract). Assembly-tunable.
|
||||||
///
|
///
|
||||||
/// Default: `DEFAULT_MAX_EXEC_COMMAND_LEN`.
|
/// Default: `DEFAULT_MAX_EXEC_COMMAND_LEN`.
|
||||||
pub max_exec_command_len: usize,
|
pub max_exec_command_len: usize,
|
||||||
/// Maximal number of allowed authentication attempts — mapped to
|
/// Maximal number of allowed authentication attempts, mapped to
|
||||||
/// russh's `max_auth_attempts` (server direction). Default `10`
|
/// russh's `max_auth_attempts` (server direction). Default `10`
|
||||||
/// per ADR-004 decision 6: the OpenSSH counting `none`-probe
|
/// per ADR-004 decision 6: the OpenSSH counting `none`-probe
|
||||||
/// consumes one attempt, so `10` leaves `IdentitiesOnly`-style
|
/// consumes one attempt, so `10` leaves `IdentitiesOnly`-style
|
||||||
/// clients at least nine real key attempts.
|
/// clients at least nine real key attempts.
|
||||||
pub max_auth_attempts: usize,
|
pub max_auth_attempts: usize,
|
||||||
/// The initial channel receive window — mapped to russh's
|
/// The initial channel receive window, mapped to russh's
|
||||||
/// `window_size` (per-channel memory bound).
|
/// `window_size` (per-channel memory bound).
|
||||||
///
|
///
|
||||||
/// Default: 2 MiB (russh's own default posture, kept).
|
/// Default: 2 MiB (russh's own default posture, kept).
|
||||||
pub window_size: u32,
|
pub window_size: u32,
|
||||||
/// The maximal size of a single SSH packet — mapped to russh's
|
/// The maximal size of a single SSH packet, mapped to russh's
|
||||||
/// `maximum_packet_size` (per-channel record-size bound).
|
/// `maximum_packet_size` (per-channel record-size bound).
|
||||||
///
|
///
|
||||||
/// Default: 32 KiB (russh's own default posture, kept).
|
/// Default: 32 KiB (russh's own default posture, kept).
|
||||||
pub maximum_packet_size: u32,
|
pub maximum_packet_size: u32,
|
||||||
/// Per-channel unprocessed-message buffer — mapped to russh's
|
/// Per-channel unprocessed-message buffer, mapped to russh's
|
||||||
/// `channel_buffer_size` (the backpressure bound); messages past
|
/// `channel_buffer_size` (the backpressure bound); messages past
|
||||||
/// it propagate backpressure to the TCP stream, never an
|
/// it propagate backpressure to the TCP stream, never an
|
||||||
/// unbounded buffer.
|
/// unbounded buffer.
|
||||||
///
|
///
|
||||||
/// Default: 100 (russh's own default posture, kept).
|
/// Default: 100 (russh's own default posture, kept).
|
||||||
pub channel_buffer_size: usize,
|
pub channel_buffer_size: usize,
|
||||||
/// The session wall clock — the connection is garbage-collected
|
/// The session wall clock; the connection is garbage-collected
|
||||||
/// after this long with no activity. Mapped to russh's
|
/// after this long with no activity. Mapped to russh's
|
||||||
/// `Option<inactivity_timeout>` as always-set (`Some(value)`) in
|
/// `Option<inactivity_timeout>` as always-set (`Some(value)`) in
|
||||||
/// wave 2/4: the bounded-resources policy's "set
|
/// wave 2/4: the bounded-resources policy's "set
|
||||||
/// (assembly-tunable)" posture — the crate never constructs a
|
/// (assembly-tunable)" posture; the crate never constructs a
|
||||||
/// session with no wall clock. Assembly-tunable.
|
/// session with no wall clock. Assembly-tunable.
|
||||||
///
|
///
|
||||||
/// Default: `DEFAULT_INACTIVITY_TIMEOUT`.
|
/// Default: `DEFAULT_INACTIVITY_TIMEOUT`.
|
||||||
@@ -142,13 +142,13 @@ pub struct Limits {
|
|||||||
/// Client-direction keepalive interval (cloud fleets): the
|
/// Client-direction keepalive interval (cloud fleets): the
|
||||||
/// dead-peer probe, mapped to russh's `keepalive_interval`.
|
/// dead-peer probe, mapped to russh's `keepalive_interval`.
|
||||||
///
|
///
|
||||||
/// Default: `Some(15 s)` — the reverse-runner-evidence posture
|
/// Default: `Some(15 s)`, the reverse-runner-evidence posture
|
||||||
/// (OpenSSH `ServerAliveInterval=15` parity): with russh's own
|
/// (OpenSSH `ServerAliveInterval=15` parity): with russh's own
|
||||||
/// default (`None`) a dead NAT'd connection sits unnoticed until
|
/// default (`None`) a dead NAT'd connection sits unnoticed until
|
||||||
/// an exec health check fails (`reverse-runner.md` lesson 3).
|
/// an exec health check fails (`reverse-runner.md` lesson 3).
|
||||||
/// `None` is available to the assembly but is not the posture.
|
/// `None` is available to the assembly but is not the posture.
|
||||||
pub keepalive_interval: Option<Duration>,
|
pub keepalive_interval: Option<Duration>,
|
||||||
/// How many unanswered keepalives close the connection — mapped
|
/// How many unanswered keepalives close the connection, mapped
|
||||||
/// to russh's `keepalive_max`.
|
/// to russh's `keepalive_max`.
|
||||||
///
|
///
|
||||||
/// Default: `3` (OpenSSH `ServerAliveCountMax=3` parity, the
|
/// Default: `3` (OpenSSH `ServerAliveCountMax=3` parity, the
|
||||||
@@ -202,32 +202,32 @@ impl Default for Limits {
|
|||||||
|
|
||||||
/// The `session` feature's alktty seam handle (`mapping.md` §The
|
/// The `session` feature's alktty seam handle (`mapping.md` §The
|
||||||
/// seams): what `drive_session[_pre_negotiated]` takes that the
|
/// seams): what `drive_session[_pre_negotiated]` takes that the
|
||||||
/// assembly composes (alktty 0.8.1's `adapter.rs:205-241` signature).
|
/// assembly composes (alktty 0.8.2's `adapter.rs:205-241` signature).
|
||||||
/// **Shape decision** (recorded at `w3-seam-config`): a whole-`Option`
|
/// **Shape decision** (recorded at `w3-seam-config`): a whole-`Option`
|
||||||
/// struct per seam, not flat `Option` fields — each seam is consumed
|
/// struct per seam, not flat `Option` fields; each seam is consumed
|
||||||
/// atomically by exactly one seam call, so a struct admits no
|
/// atomically by exactly one seam call, so a struct admits no
|
||||||
/// half-supplied seam states and the unset-declines contract reads as
|
/// half-supplied seam states and the unset-declines contract reads as
|
||||||
/// one `Option`: **`ServeConfig::session_seam` unset → the session
|
/// one `Option`: **`ServeConfig::session_seam` unset → the session
|
||||||
/// rows decline even though the `session` feature is on** (the
|
/// rows decline even though the `session` feature is on** (the
|
||||||
/// decline matrix's config-driven note), checked at request time by
|
/// decline matrix's config-driven note), checked at request time by
|
||||||
/// the mappings — no construction-time validation. Cloned into each
|
/// the mappings; no construction-time validation. Cloned into each
|
||||||
/// spawned serving task (all payload types `Arc` or plain data).
|
/// spawned serving task (all payload types `Arc` or plain data).
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SessionSeam {
|
pub struct SessionSeam {
|
||||||
/// The alktty backends map — the `TtyAdapter`-style registry keyed
|
/// The alktty backends map, the `TtyAdapter`-style registry keyed
|
||||||
/// by the `NegotiateRequest.backend` selector string. **Assembled
|
/// by the `NegotiateRequest.backend` selector string. **Assembled
|
||||||
/// by the assembly**: alkssh never touches a real backend, and the
|
/// by the assembly**: alkssh never touches a real backend, and the
|
||||||
/// `local` backend shapes stay alktty-side (this dep's `local`
|
/// `local` backend shapes stay alktty-side (this dep's `local`
|
||||||
/// feature is not enabled — the map holds assembly-supplied
|
/// feature is not enabled; the map holds assembly-supplied
|
||||||
/// implementations of alktty's [`TtyBackend`] only).
|
/// implementations of alktty's [`TtyBackend`] only).
|
||||||
pub backends: Arc<HashMap<String, Arc<dyn TtyBackend>>>,
|
pub backends: Arc<HashMap<String, Arc<dyn TtyBackend>>>,
|
||||||
/// The backend **selector** — the `NegotiateRequest.backend`
|
/// The backend **selector**, the `NegotiateRequest.backend`
|
||||||
/// string alkssh writes into the locally-constructed negotiation
|
/// string alkssh writes into the locally-constructed negotiation
|
||||||
/// (`w3-tty-session`'s negotiation rows; the wire command/args
|
/// (`w3-tty-session`'s negotiation rows; the wire command/args
|
||||||
/// never reach it). Assembly-supplied, never wire-derived.
|
/// never reach it). Assembly-supplied, never wire-derived.
|
||||||
pub backend: String,
|
pub backend: String,
|
||||||
/// The ADR-050 ownership provider — passed through
|
/// The ADR-050 ownership provider, passed through
|
||||||
/// `drive_session[_pre_negotiated]` verbatim (the family ACL
|
/// `drive_session[_pre_negotiated]` verbatim (the family ACL
|
||||||
/// layer riding alongside; an alkcall type, no new dep).
|
/// layer riding alongside; an alkcall type, no new dep).
|
||||||
pub ownership: Option<Arc<dyn OwnershipProvider>>,
|
pub ownership: Option<Arc<dyn OwnershipProvider>>,
|
||||||
@@ -248,27 +248,27 @@ impl std::fmt::Debug for SessionSeam {
|
|||||||
|
|
||||||
/// The `git` feature's alkgit seam handle (`mapping.md` §The seams):
|
/// The `git` feature's alkgit seam handle (`mapping.md` §The seams):
|
||||||
/// what `serve_duplex_resolved` takes that the assembly composes
|
/// what `serve_duplex_resolved` takes that the assembly composes
|
||||||
/// (alkgit 0.1.0's `duplex.rs:1412-1426` signature — the session
|
/// (alkgit 0.1.0's `duplex.rs:1412-1426` signature: the session
|
||||||
/// budget, the four backend traits, the caller-owned pipeline, the
|
/// budget, the four backend traits, the caller-owned pipeline, the
|
||||||
/// push-options gate; the `limits` field landed with `w3-git-mapping`
|
/// push-options gate; the `limits` field landed with `w3-git-mapping`
|
||||||
/// — the first seam consumer — as a `w3-seam-config` course
|
/// the first seam consumer, as a `w3-seam-config` course
|
||||||
/// correction: alkgit's ADR-009 budget is assembly config, the door
|
/// correction: alkgit's ADR-009 budget is assembly config, the door
|
||||||
/// never invents it). Same whole-`Option`-struct decision as
|
/// never invents it). Same whole-`Option`-struct decision as
|
||||||
/// [`SessionSeam`]:
|
/// [`SessionSeam`]:
|
||||||
/// **`ServeConfig::git_seam` unset → the git row declines even though
|
/// **`ServeConfig::git_seam` unset → the git row declines even though
|
||||||
/// the `git` feature is on**, checked at request time by the
|
/// the `git` feature is on**, checked at request time by the
|
||||||
/// mappings — no construction-time validation.
|
/// mappings; no construction-time validation.
|
||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GitSeam {
|
pub struct GitSeam {
|
||||||
/// The alkgit session budget (alkgit ADR-009's per-session bundle —
|
/// The alkgit session budget (alkgit ADR-009's per-session bundle:
|
||||||
/// wall clock, pack-size caps, negotiation rounds): **constructed
|
/// wall clock, pack-size caps, negotiation rounds): **constructed
|
||||||
/// by the assembly** (alkgit's own contract — the door never
|
/// by the assembly** (alkgit's own contract: the door never
|
||||||
/// invents budget values), passed into `serve_duplex_resolved`
|
/// invents budget values), passed into `serve_duplex_resolved`
|
||||||
/// verbatim (`w3-git-mapping`). Carried `Copy`; [`alkgit::Limits`]'s
|
/// verbatim (`w3-git-mapping`). Carried `Copy`; [`alkgit::Limits`]'s
|
||||||
/// own defaults are the assembly's baseline.
|
/// own defaults are the assembly's baseline.
|
||||||
pub limits: alkgit::Limits,
|
pub limits: alkgit::Limits,
|
||||||
/// The repo-id resolution trait (`GitRegistry::resolve`) — the
|
/// The repo-id resolution trait (`GitRegistry::resolve`), the
|
||||||
/// ADR-007 pre-protocol gate's other half; alkssh never resolves
|
/// ADR-007 pre-protocol gate's other half; alkssh never resolves
|
||||||
/// (the collapsed-generic-denial posture lives in alkgit).
|
/// (the collapsed-generic-denial posture lives in alkgit).
|
||||||
pub registry: Arc<dyn GitRegistry>,
|
pub registry: Arc<dyn GitRegistry>,
|
||||||
@@ -278,13 +278,13 @@ pub struct GitSeam {
|
|||||||
/// The pack generation + negotiation-ack trait
|
/// The pack generation + negotiation-ack trait
|
||||||
/// (`generate`/`common_haves`).
|
/// (`generate`/`common_haves`).
|
||||||
pub packgen: Arc<dyn GitPackGen>,
|
pub packgen: Arc<dyn GitPackGen>,
|
||||||
/// The pack-preparation trait (`prepare` — never applies; the
|
/// The pack-preparation trait (`prepare` never applies; the
|
||||||
/// `GitRefs::apply_updates` CAS stays the single ingestion home).
|
/// `GitRefs::apply_updates` CAS stays the single ingestion home).
|
||||||
pub ingest: Arc<dyn GitPackIngest>,
|
pub ingest: Arc<dyn GitPackIngest>,
|
||||||
/// The assembler's push-options flag (alkgit's ADR-013 §11 gate,
|
/// The assembler's push-options flag (alkgit's ADR-013 §11 gate,
|
||||||
/// passed through verbatim) — `false` in the v1 default.
|
/// passed through verbatim), `false` in the v1 default.
|
||||||
pub push_options_negotiated: bool,
|
pub push_options_negotiated: bool,
|
||||||
/// The caller-owned, server-wide pipeline permit — alkgit's
|
/// The caller-owned, server-wide pipeline permit: alkgit's
|
||||||
/// pack-pipeline concurrency bound, *shared across every session
|
/// pack-pipeline concurrency bound, *shared across every session
|
||||||
/// the serving direction takes* (the caller keeps its own handle;
|
/// the serving direction takes* (the caller keeps its own handle;
|
||||||
/// `Arc` for the share). One per `DialConfig`-independent serve
|
/// `Arc` for the share). One per `DialConfig`-independent serve
|
||||||
@@ -308,7 +308,7 @@ impl std::fmt::Debug for GitSeam {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The `tunnel` feature's `-R` listen-leg registration seam (the
|
/// The `tunnel` feature's `-R` listen-leg registration seam (the
|
||||||
/// `w3-tunnel-listen` shape — the wave-3 seam-config's second
|
/// `w3-tunnel-listen` shape, the wave-3 seam-config's second
|
||||||
/// course-correction, resolved per its own decision record): the
|
/// course-correction, resolved per its own decision record): the
|
||||||
/// assembly-supplied `Arc<ChannelClient>` alone cannot carry the
|
/// assembly-supplied `Arc<ChannelClient>` alone cannot carry the
|
||||||
/// door-side acceptance of the fabric's inbound forwarded opens,
|
/// door-side acceptance of the fabric's inbound forwarded opens,
|
||||||
@@ -324,11 +324,11 @@ impl std::fmt::Debug for GitSeam {
|
|||||||
/// **`ServeConfig::tunnel_listen_seam` unset → the `-R` listen rows
|
/// **`ServeConfig::tunnel_listen_seam` unset → the `-R` listen rows
|
||||||
/// (`tcpip_forward` / `streamlocal_forward` / both cancel forms)
|
/// (`tcpip_forward` / `streamlocal_forward` / both cancel forms)
|
||||||
/// decline even under the `tunnel` feature with a fabric leg present**
|
/// decline even under the `tunnel` feature with a fabric leg present**
|
||||||
/// — an egress-only door (the POC-3 stage-C posture) is legitimate
|
/// : an egress-only door (the POC-3 stage-C posture) is legitimate
|
||||||
/// config; the feature alone configures nothing. Checked at request
|
/// config; the feature alone configures nothing. Checked at request
|
||||||
/// time by the mappings; no construction-time validation. The
|
/// time by the mappings; no construction-time validation. The
|
||||||
/// registration itself (the alkcall openable registration the seam
|
/// registration itself (the alkcall openable registration the seam
|
||||||
/// fronts) is alkssh-side work at [`crate::SshAdapter::new`] — a
|
/// fronts) is alkssh-side work at [`crate::SshAdapter::new`]; a
|
||||||
/// failed registration is a failed construction (fail-closed, never a
|
/// failed registration is a failed construction (fail-closed, never a
|
||||||
/// half-configured adapter).
|
/// half-configured adapter).
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
@@ -336,7 +336,7 @@ pub struct TunnelListenSeam {
|
|||||||
/// The **shared** per-connection `OperationRegistry` the fabric
|
/// The **shared** per-connection `OperationRegistry` the fabric
|
||||||
/// leg's inbound ops dispatch against: the exact `Arc` the
|
/// leg's inbound ops dispatch against: the exact `Arc` the
|
||||||
/// assembly passed in this leg's `ChannelClient` construction
|
/// assembly passed in this leg's `ChannelClient` construction
|
||||||
/// (`ServingConfig.registry`) — alkssh registers the
|
/// (`ServingConfig.registry`): alkssh registers the
|
||||||
/// `tunnel/forwarded` openable into it, and alkcall's dispatch
|
/// `tunnel/forwarded` openable into it, and alkcall's dispatch
|
||||||
/// reads it per call (the W2 post-hoc-registration posture). The
|
/// reads it per call (the W2 post-hoc-registration posture). The
|
||||||
/// door's own ACL entry rides the registration; arrivals lacking
|
/// door's own ACL entry rides the registration; arrivals lacking
|
||||||
@@ -355,7 +355,7 @@ use alkcall::registry::registration::OperationRegistry;
|
|||||||
impl TunnelListenSeam {
|
impl TunnelListenSeam {
|
||||||
/// Builds the seam over the assembly's shared per-connection
|
/// Builds the seam over the assembly's shared per-connection
|
||||||
/// registry (the `ServingConfig.registry` the same fabric leg's
|
/// registry (the `ServingConfig.registry` the same fabric leg's
|
||||||
/// `ChannelClient` was constructed with — one `Arc`, both sides).
|
/// `ChannelClient` was constructed with: one `Arc`, both sides).
|
||||||
pub fn new(registry: Arc<OperationRegistry>) -> Self {
|
pub fn new(registry: Arc<OperationRegistry>) -> Self {
|
||||||
Self { registry }
|
Self { registry }
|
||||||
}
|
}
|
||||||
@@ -372,22 +372,22 @@ impl std::fmt::Debug for TunnelListenSeam {
|
|||||||
|
|
||||||
/// The server direction's assembly-supplied serving configuration
|
/// The server direction's assembly-supplied serving configuration
|
||||||
/// (`overview.md` §Server stack): everything wave-2's serving loop
|
/// (`overview.md` §Server stack): everything wave-2's serving loop
|
||||||
/// takes at construction. Pure data + injected handles — no russh
|
/// takes at construction. Pure data + injected handles; no russh
|
||||||
/// `Config` is built here (wave 2), no serving loop exists yet.
|
/// `Config` is built here (wave 2), no serving loop exists yet.
|
||||||
///
|
///
|
||||||
/// Construction carries no validation beyond the required handles:
|
/// Construction carries no validation beyond the required handles:
|
||||||
/// the unfeatured/unset capability surfaces are structural `Option`s
|
/// the unfeatured/unset capability surfaces are structural `Option`s
|
||||||
/// that decline at request time (the fail-closed contract) — see
|
/// that decline at request time (the fail-closed contract); see
|
||||||
/// [`ServeConfig::shell_argv`] and [`ServeConfig::channel_client`].
|
/// [`ServeConfig::shell_argv`] and [`ServeConfig::channel_client`].
|
||||||
pub struct ServeConfig {
|
pub struct ServeConfig {
|
||||||
/// OQ-08 field 1: the shell argv, run verbatim for
|
/// OQ-08 field 1: the shell argv, run verbatim for
|
||||||
/// `shell_request` (`NegotiateRequest.cmd`). No crate default —
|
/// `shell_request` (`NegotiateRequest.cmd`). No crate default:
|
||||||
/// **unset → `shell_request` declines with a clean error, never a
|
/// **unset → `shell_request` declines with a clean error, never a
|
||||||
/// fallback**: the crate is not an sshd and has no account model
|
/// fallback**: the crate is not an sshd and has no account model
|
||||||
/// from which a shell could be derived, so any baked-in shell
|
/// from which a shell could be derived, so any baked-in shell
|
||||||
/// would be deployment policy smuggled into the crate. A
|
/// would be deployment policy smuggled into the crate. A
|
||||||
/// `session` build with this unset serves neither shell nor exec
|
/// `session` build with this unset serves neither shell nor exec
|
||||||
/// requests (the git row is unaffected — parsing never needs a
|
/// requests (the git row is unaffected, since parsing never needs a
|
||||||
/// shell).
|
/// shell).
|
||||||
///
|
///
|
||||||
/// Set to `Some(vec)` to serve the shell row (feature-gated at
|
/// Set to `Some(vec)` to serve the shell row (feature-gated at
|
||||||
@@ -396,33 +396,33 @@ pub struct ServeConfig {
|
|||||||
/// OQ-08 field 2: the exec wrapper argv (platform-neutral, e.g.
|
/// OQ-08 field 2: the exec wrapper argv (platform-neutral, e.g.
|
||||||
/// `["/bin/sh", "-c"]`). A non-git `exec_request`'s wire command
|
/// `["/bin/sh", "-c"]`). A non-git `exec_request`'s wire command
|
||||||
/// string is appended as the final argv element
|
/// string is appended as the final argv element
|
||||||
/// (`wrapper ++ [cmd]`) — vanilla `ssh host "cmd"` quoting
|
/// (`wrapper ++ [cmd]`), the vanilla `ssh host "cmd"` quoting
|
||||||
/// semantics; command strings are never naively split. No crate
|
/// semantics; command strings are never naively split. No crate
|
||||||
/// default — **unset → the non-git exec row declines with a
|
/// default: **unset → the non-git exec row declines with a
|
||||||
/// clean error, never a fallback**.
|
/// clean error, never a fallback**.
|
||||||
///
|
///
|
||||||
/// Set to `Some(vec)` to serve the pipe-exec row (feature-gated
|
/// Set to `Some(vec)` to serve the pipe-exec row (feature-gated
|
||||||
/// at wave 2: only under the `session` feature).
|
/// at wave 2: only under the `session` feature).
|
||||||
pub exec_wrapper_argv: Option<Vec<String>>,
|
pub exec_wrapper_argv: Option<Vec<String>>,
|
||||||
/// The OQ-09 posture's assembly-supplied working directory for
|
/// The OQ-09 posture's assembly-supplied working directory for
|
||||||
/// alktty `NegotiateRequest.cwd` — **never wire-derived**
|
/// alktty `NegotiateRequest.cwd`, **never wire-derived**
|
||||||
/// (`env_request` declines unconditionally; wire env is not
|
/// (`env_request` declines unconditionally; wire env is not
|
||||||
/// family config). Unset → the backend-side default applies.
|
/// family config). Unset → the backend-side default applies.
|
||||||
pub session_cwd: Option<PathBuf>,
|
pub session_cwd: Option<PathBuf>,
|
||||||
/// The OQ-09 posture's assembly-supplied environment for
|
/// The OQ-09 posture's assembly-supplied environment for
|
||||||
/// `NegotiateRequest.env` — **never wire-derived**. Empty by
|
/// `NegotiateRequest.env`, **never wire-derived**. Empty by
|
||||||
/// default.
|
/// default.
|
||||||
pub session_env: BTreeMap<String, String>,
|
pub session_env: BTreeMap<String, String>,
|
||||||
/// The `session` feature's alktty seam handle ([`SessionSeam`] —
|
/// The `session` feature's alktty seam handle ([`SessionSeam`],
|
||||||
/// the backends map + backend selector + ownership provider the
|
/// the backends map + backend selector + ownership provider the
|
||||||
/// `drive_session[_pre_negotiated]` rows take). **Unset → the
|
/// `drive_session[_pre_negotiated]` rows take). **Unset → the
|
||||||
/// session rows decline even though the `session` feature is on**
|
/// session rows decline even though the `session` feature is on**
|
||||||
/// (the decline matrix's config-driven note; the feature alone
|
/// (the decline matrix's config-driven note; the feature alone
|
||||||
/// configures nothing). Wave-3 extension of the wave-1 config
|
/// configures nothing). Wave-3 extension of the wave-1 config
|
||||||
/// surface — shape decision recorded on [`SessionSeam`].
|
/// surface; shape decision recorded on [`SessionSeam`].
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
pub session_seam: Option<SessionSeam>,
|
pub session_seam: Option<SessionSeam>,
|
||||||
/// The `git` feature's alkgit seam handle ([`GitSeam`] — the four
|
/// The `git` feature's alkgit seam handle ([`GitSeam`], the four
|
||||||
/// backend traits + the caller-owned pipeline + the push-options
|
/// backend traits + the caller-owned pipeline + the push-options
|
||||||
/// flag the `serve_duplex_resolved` row takes). **Unset → the git
|
/// flag the `serve_duplex_resolved` row takes). **Unset → the git
|
||||||
/// row declines even though the `git` feature is on** (same
|
/// row declines even though the `git` feature is on** (same
|
||||||
@@ -430,33 +430,33 @@ pub struct ServeConfig {
|
|||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
pub git_seam: Option<GitSeam>,
|
pub git_seam: Option<GitSeam>,
|
||||||
/// The injected identity provider (alkcall's
|
/// The injected identity provider (alkcall's
|
||||||
/// `IdentityProvider::resolve_from_fingerprint`) — the assembly
|
/// `IdentityProvider::resolve_from_fingerprint`), the assembly
|
||||||
/// supplies it; the crate never resolves identities itself and
|
/// supplies it; the crate never resolves identities itself and
|
||||||
/// holds no identity model.
|
/// holds no identity model.
|
||||||
pub identity_provider: Arc<dyn IdentityProvider>,
|
pub identity_provider: Arc<dyn IdentityProvider>,
|
||||||
/// The injected host-key source (the wave-1
|
/// The injected host-key source (the wave-1
|
||||||
/// [`HostKeySource`] seam, decrypt-at-construction per ADR-005
|
/// [`HostKeySource`] seam, decrypt-at-construction per ADR-005
|
||||||
/// decision 6) — feeds wave-2's `Config::keys`; a load failure
|
/// decision 6) feeds wave-2's `Config::keys`; a load failure
|
||||||
/// means no server is constructed, never a half-configured one.
|
/// means no server is constructed, never a half-configured one.
|
||||||
pub host_key_source: Arc<dyn HostKeySource>,
|
pub host_key_source: Arc<dyn HostKeySource>,
|
||||||
/// The tunnel route-1 bridge's family-fabric entry: a held
|
/// The tunnel route-1 bridge's family-fabric entry: a held
|
||||||
/// alkcall `ChannelClient` (`TunnelSession::open_direct` runs on
|
/// alkcall `ChannelClient` (`TunnelSession::open_direct` runs on
|
||||||
/// it). **Absent → every tunnel row declines with a clean error
|
/// it). **Absent → every tunnel row declines with a clean error
|
||||||
/// even under the `tunnel` feature** (the feature alone configures
|
/// even under the `tunnel` feature** (the feature alone configures
|
||||||
/// nothing — the fabric handle is the row's other half). The
|
/// nothing; the fabric handle is the row's other half). The
|
||||||
/// present handle is still gated by the `tunnel` feature at wave
|
/// present handle is still gated by the `tunnel` feature at wave
|
||||||
/// 2: absent + unfeatured both decline; present + featured
|
/// 2: absent + unfeatured both decline; present + featured
|
||||||
/// serves.
|
/// serves.
|
||||||
pub channel_client: Option<Arc<ChannelClient>>,
|
pub channel_client: Option<Arc<ChannelClient>>,
|
||||||
/// The `-R` listen leg's registration seam ([`TunnelListenSeam`] —
|
/// The `-R` listen leg's registration seam ([`TunnelListenSeam`],
|
||||||
/// the shared per-connection registry the fabric's inbound
|
/// the shared per-connection registry the fabric's inbound
|
||||||
/// forwarded opens dispatch against). **Unset → the listen rows
|
/// forwarded opens dispatch against). **Unset → the listen rows
|
||||||
/// decline even under the `tunnel` feature with a fabric leg
|
/// decline even under the `tunnel` feature with a fabric leg
|
||||||
/// present** (the egress-only door is legitimate config — the
|
/// present** (the egress-only door is legitimate config, the
|
||||||
/// POC-3 stage-C posture; shape decision recorded on the type).
|
/// POC-3 stage-C posture; shape decision recorded on the type).
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
pub tunnel_listen_seam: Option<TunnelListenSeam>,
|
pub tunnel_listen_seam: Option<TunnelListenSeam>,
|
||||||
/// The listen leg's per-arrival bookkeeping — installed by
|
/// The listen leg's per-arrival bookkeeping, installed by
|
||||||
/// [`crate::SshAdapter::new`] when the fabric leg + seam are both
|
/// [`crate::SshAdapter::new`] when the fabric leg + seam are both
|
||||||
/// present (the registration site). Crate-private: never assembly
|
/// present (the registration site). Crate-private: never assembly
|
||||||
/// data.
|
/// data.
|
||||||
@@ -469,10 +469,10 @@ pub struct ServeConfig {
|
|||||||
|
|
||||||
impl ServeConfig {
|
impl ServeConfig {
|
||||||
/// Builds a serve-config from the two injected handles the
|
/// Builds a serve-config from the two injected handles the
|
||||||
/// serving direction can never default — the identity provider
|
/// serving direction can never default, the identity provider
|
||||||
/// and the host-key source. The OQ-08 fields start unset (their
|
/// and the host-key source. The OQ-08 fields start unset (their
|
||||||
/// rows decline), the tunnel handle starts absent (its rows
|
/// rows decline), the tunnel handle starts absent (its rows
|
||||||
/// decline), and `limits` starts at [`Limits::default()`] —
|
/// decline), and `limits` starts at [`Limits::default()`],
|
||||||
/// override the `pub` fields from the assembly.
|
/// override the `pub` fields from the assembly.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
identity_provider: Arc<dyn IdentityProvider>,
|
identity_provider: Arc<dyn IdentityProvider>,
|
||||||
@@ -500,14 +500,14 @@ impl ServeConfig {
|
|||||||
|
|
||||||
/// Whether the `shell_request` row is configurable at all in this
|
/// Whether the `shell_request` row is configurable at all in this
|
||||||
/// configuration (`Some` under the `session` feature): `false`
|
/// configuration (`Some` under the `session` feature): `false`
|
||||||
/// means the row declines — the unset-declines contract, checked
|
/// means the row declines; the unset-declines contract, checked
|
||||||
/// at request time by the wave-2 dispatch, never here.
|
/// at request time by the wave-2 dispatch, never here.
|
||||||
pub fn shell_configured(&self) -> bool {
|
pub fn shell_configured(&self) -> bool {
|
||||||
self.shell_argv.is_some()
|
self.shell_argv.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the non-git `exec_request` (pipe) row is configurable
|
/// Whether the non-git `exec_request` (pipe) row is configurable
|
||||||
/// at all in this configuration: `false` means the row declines —
|
/// at all in this configuration: `false` means the row declines;
|
||||||
/// the unset-declines contract, checked at request time by the
|
/// the unset-declines contract, checked at request time by the
|
||||||
/// wave-2 dispatch, never here.
|
/// wave-2 dispatch, never here.
|
||||||
pub fn exec_wrapper_configured(&self) -> bool {
|
pub fn exec_wrapper_configured(&self) -> bool {
|
||||||
@@ -524,7 +524,7 @@ impl ServeConfig {
|
|||||||
/// Whether the `-R` listen rows' registration seam is configured
|
/// Whether the `-R` listen rows' registration seam is configured
|
||||||
/// in this configuration ([`TunnelListenSeam`] present; the
|
/// in this configuration ([`TunnelListenSeam`] present; the
|
||||||
/// `tunnel` feature gate applies at the dispatch on top): `false`
|
/// `tunnel` feature gate applies at the dispatch on top): `false`
|
||||||
/// means the listen rows decline — the unset-declines contract,
|
/// means the listen rows decline; the unset-declines contract,
|
||||||
/// checked at request time by the mappings, never here.
|
/// checked at request time by the mappings, never here.
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
pub fn tunnel_listen_seam_configured(&self) -> bool {
|
pub fn tunnel_listen_seam_configured(&self) -> bool {
|
||||||
@@ -534,7 +534,7 @@ impl ServeConfig {
|
|||||||
/// Whether the session rows' family seam is configured in this
|
/// Whether the session rows' family seam is configured in this
|
||||||
/// configuration ([`SessionSeam`] present; the `session` feature
|
/// configuration ([`SessionSeam`] present; the `session` feature
|
||||||
/// gate applies at the dispatch on top): `false` means the rows
|
/// gate applies at the dispatch on top): `false` means the rows
|
||||||
/// decline — the unset-declines contract, checked at request time
|
/// decline; the unset-declines contract, checked at request time
|
||||||
/// by the wave-3 mappings, never here.
|
/// by the wave-3 mappings, never here.
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
pub fn session_seam_configured(&self) -> bool {
|
pub fn session_seam_configured(&self) -> bool {
|
||||||
@@ -543,8 +543,8 @@ impl ServeConfig {
|
|||||||
|
|
||||||
/// Whether the git row's family seam is configured in this
|
/// Whether the git row's family seam is configured in this
|
||||||
/// configuration ([`GitSeam`] present; the `git` feature gate
|
/// configuration ([`GitSeam`] present; the `git` feature gate
|
||||||
/// applies at the dispatch on top): `false` means the row declines
|
/// applies at the dispatch on top): `false` means the row declines;
|
||||||
/// — the unset-declines contract, checked at request time by the
|
/// the unset-declines contract, checked at request time by the
|
||||||
/// wave-3 mappings, never here.
|
/// wave-3 mappings, never here.
|
||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
pub fn git_seam_configured(&self) -> bool {
|
pub fn git_seam_configured(&self) -> bool {
|
||||||
@@ -581,21 +581,21 @@ impl std::fmt::Debug for ServeConfig {
|
|||||||
|
|
||||||
/// The client direction's dial configuration (`client.md` §The dial
|
/// The client direction's dial configuration (`client.md` §The dial
|
||||||
/// surface): everything wave-4's dial surface takes. Pure data +
|
/// surface): everything wave-4's dial surface takes. Pure data +
|
||||||
/// injected handles — the russh client `Config` construction and the
|
/// injected handles; the russh client `Config` construction and the
|
||||||
/// dial itself live in the crate's dial module (the `w4-dial`
|
/// dial itself live in the crate's dial module (the `w4-dial`
|
||||||
/// surface).
|
/// surface).
|
||||||
///
|
///
|
||||||
/// Construction carries no validation: the key source's own
|
/// Construction carries no validation: the key source's own
|
||||||
/// load path is fail-closed ([`KeySource`] — locked/absent/decryption
|
/// load path is fail-closed ([`KeySource`]: locked/absent/decryption
|
||||||
/// failures are all no-dial outcomes), and the host-key policy's
|
/// failures are all no-dial outcomes), and the host-key policy's
|
||||||
/// default is fail-closed ([crate::host_key::RejectAll]).
|
/// default is fail-closed ([crate::host_key::RejectAll]).
|
||||||
///
|
///
|
||||||
/// **The `username` field** (the task's sanctioned
|
/// **The `username` field** (the task's sanctioned
|
||||||
/// `w3-seam-config`-pattern course-correction — the only amendment the
|
/// `w3-seam-config`-pattern course-correction, the only amendment the
|
||||||
/// wave-1 shape needed): the USERAUTH_REQUEST's user string. The
|
/// wave-1 shape needed): the USERAUTH_REQUEST's user string. The
|
||||||
/// wave-1 shape carried everything `client.md` §The dial surface names
|
/// wave-1 shape carried everything `client.md` §The dial surface names
|
||||||
/// but structurally missed this one: russh's `authenticate_publickey`
|
/// but structurally missed this one: russh's `authenticate_publickey`
|
||||||
/// requires a user, and the remote end evaluates it — the family
|
/// requires a user, and the remote end evaluates it; the family
|
||||||
/// identity model stays fingerprint-only forever (`auth.md` §Host-key;
|
/// identity model stays fingerprint-only forever (`auth.md` §Host-key;
|
||||||
/// the username is not identity and carries no routing semantics,
|
/// the username is not identity and carries no routing semantics,
|
||||||
/// OQ-03), so this field is pure dial data the assembly supplies. No
|
/// OQ-03), so this field is pure dial data the assembly supplies. No
|
||||||
@@ -603,28 +603,28 @@ impl std::fmt::Debug for ServeConfig {
|
|||||||
/// smuggled into the crate); an empty string is accepted as data and
|
/// smuggled into the crate); an empty string is accepted as data and
|
||||||
/// is the remote end's to refuse.
|
/// is the remote end's to refuse.
|
||||||
pub struct DialConfig {
|
pub struct DialConfig {
|
||||||
/// The dial target — `host:port` as russh's connect surface
|
/// The dial target: `host:port` as russh's connect surface
|
||||||
/// takes it (the reverse-runner shape: vanilla `sshd` on the
|
/// takes it (the reverse-runner shape: vanilla `sshd` on the
|
||||||
/// fleet box). The same address labels the host-key policy's
|
/// fleet box). The same address labels the host-key policy's
|
||||||
/// presented-key decisions (host = the parsed host, port = the
|
/// presented-key decisions (host = the parsed host, port = the
|
||||||
/// parsed port).
|
/// parsed port).
|
||||||
pub address: String,
|
pub address: String,
|
||||||
/// The user the dial authenticates as, verbatim on the
|
/// The user the dial authenticates as, verbatim on the
|
||||||
/// USERAUTH_REQUEST — not identity (the fingerprint is; ADR-004
|
/// USERAUTH_REQUEST, not identity (the fingerprint is; ADR-004
|
||||||
/// decision 5's posture, mirrored client-side), never logged (the
|
/// decision 5's posture, mirrored client-side), never logged (the
|
||||||
/// crate's event vocabulary carries no username). See the type
|
/// crate's event vocabulary carries no username). See the type
|
||||||
/// docs.
|
/// docs.
|
||||||
pub username: String,
|
pub username: String,
|
||||||
/// The key source the dial authenticates with (the wave-1
|
/// The key source the dial authenticates with (the wave-1
|
||||||
/// [`KeySource`] seam) — never a static key, never env vars;
|
/// [`KeySource`] seam): never a static key, never env vars;
|
||||||
/// decrypt-at-dial, fail-closed per ADR-005.
|
/// decrypt-at-dial, fail-closed per ADR-005.
|
||||||
pub key_source: Arc<dyn KeySource>,
|
pub key_source: Arc<dyn KeySource>,
|
||||||
/// The injected host-key policy (OQ-05) — the trust decision
|
/// The injected host-key policy (OQ-05), the trust decision
|
||||||
/// surface the dial calls at russh's `check_server_key` hook.
|
/// surface the dial calls at russh's `check_server_key` hook.
|
||||||
/// The crate is policy-blind; the built-in default policy
|
/// The crate is policy-blind; the built-in default policy
|
||||||
/// ([crate::host_key::RejectAll]) connects to nothing, by design.
|
/// ([crate::host_key::RejectAll]) connects to nothing, by design.
|
||||||
pub host_key_policy: Arc<dyn HostKeyPolicy>,
|
pub host_key_policy: Arc<dyn HostKeyPolicy>,
|
||||||
/// The bounded-resource budget this dial runs under — the
|
/// The bounded-resource budget this dial runs under; the
|
||||||
/// keepalive interval/max, the wall clock and the window/packet
|
/// keepalive interval/max, the wall clock and the window/packet
|
||||||
/// knobs ride here (see [`Limits`]; the keepalive 15 s / 3
|
/// knobs ride here (see [`Limits`]; the keepalive 15 s / 3
|
||||||
/// default posture is the client direction's).
|
/// default posture is the client direction's).
|
||||||
@@ -634,7 +634,7 @@ pub struct DialConfig {
|
|||||||
impl DialConfig {
|
impl DialConfig {
|
||||||
/// Builds a dial-config from the address, the user it
|
/// Builds a dial-config from the address, the user it
|
||||||
/// authenticates as, and the two injected handles the dial
|
/// authenticates as, and the two injected handles the dial
|
||||||
/// direction can never default — the key source and the host-key
|
/// direction can never default, the key source and the host-key
|
||||||
/// policy. `limits` starts at [`Limits::default()`] (keepalive
|
/// policy. `limits` starts at [`Limits::default()`] (keepalive
|
||||||
/// 15 s / 3 posture riding inside).
|
/// 15 s / 3 posture riding inside).
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@@ -1033,7 +1033,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The wave-3 per-feature seam handles: the construction shapes, the
|
// The wave-3 per-feature seam handles: the construction shapes, the
|
||||||
// unset-declines-preserved rows and the Debug posture — each block
|
// unset-declines-preserved rows and the Debug posture; each block
|
||||||
// only runs in the build that carries its feature (compile-time
|
// only runs in the build that carries its feature (compile-time
|
||||||
// feature isolation: no git build ever touches alktty types,
|
// feature isolation: no git build ever touches alktty types,
|
||||||
// no session build ever touches alkgit types).
|
// no session build ever touches alkgit types).
|
||||||
@@ -1131,7 +1131,7 @@ mod tests {
|
|||||||
|
|
||||||
/// - *Contract stamp:* the `w3-tunnel-listen` seam shape (the
|
/// - *Contract stamp:* the `w3-tunnel-listen` seam shape (the
|
||||||
/// unset-declines contract's tunnel extension): the seam starts
|
/// unset-declines contract's tunnel extension): the seam starts
|
||||||
/// unset (the listen rows decline — an egress-only door is
|
/// unset (the listen rows decline; an egress-only door is
|
||||||
/// legitimate config), sets independently of the fabric handle,
|
/// legitimate config), sets independently of the fabric handle,
|
||||||
/// and Debug-shapes without registry internals.
|
/// and Debug-shapes without registry internals.
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
|
|||||||
+51
-51
@@ -1,75 +1,75 @@
|
|||||||
//! The exec-grammar dispatch core (`w2-dispatch-decline-core`, full
|
//! The exec-grammar dispatch core (`w2-dispatch-decline-core`, full
|
||||||
//! grammar `w3-git-mapping`) — the routing decision and the wire-decline
|
//! grammar `w3-git-mapping`): the routing decision and the wire-decline
|
||||||
//! reason shaping the per-connection handler hooks speak
|
//! reason shaping the per-connection handler hooks speak
|
||||||
//! ([`crate::handler`]).
|
//! ([`crate::handler`]).
|
||||||
//!
|
//!
|
||||||
//! **Parse-only** (ADR-006 point 3 as amended): an `exec_request`
|
//! **Parse-only** (ADR-006 point 3 as amended): an `exec_request`
|
||||||
//! parses *first* — before dispatch and before any resource
|
//! parses *first*, before dispatch and before any resource
|
||||||
//! allocation (**decline first, alloc never**, the POC-3 stage-A rule:
|
//! allocation (**decline first, alloc never**, the POC-3 stage-A rule:
|
||||||
//! a leaked channel alloc is a capability leak). The parse is total:
|
//! a leaked channel alloc is a capability leak). The parse is total:
|
||||||
//! it routes every input to exactly one of [`ExecRoute`]'s outcomes.
|
//! it routes every input to exactly one of [`ExecRoute`]'s outcomes.
|
||||||
//! The *per-length limit* ([`crate::Limits::max_exec_command_len`]) is
|
//! The *per-length limit* ([`crate::Limits::max_exec_command_len`]) is
|
||||||
//! consumed **before** this parse — the handler checks the wire byte
|
//! consumed **before** this parse: the handler checks the wire byte
|
||||||
//! length first (pre-parse, pre-allocation), so everything below
|
//! length first (pre-parse, pre-allocation), so everything below
|
||||||
//! assumes a sized input. Invalid UTF-8 parses to the pipe row
|
//! assumes a sized input. Invalid UTF-8 parses to the pipe row
|
||||||
//! (parse-then-fail, the wave-2 rule).
|
//! (parse-then-fail, the wave-2 rule).
|
||||||
//!
|
//!
|
||||||
//! ## The exec grammar (`w3-git-mapping` — the wave-2 probe's named
|
//! ## The exec grammar (`w3-git-mapping`, the wave-2 probe's named
|
||||||
//! replacement, its unit rows migrated)
|
//! replacement, its unit rows migrated)
|
||||||
//!
|
//!
|
||||||
//! What vanilla git sends over SSH (`git-upload-pack '/owner/demo'` —
|
//! What vanilla git sends over SSH (`git-upload-pack '/owner/demo'`:
|
||||||
//! single-quoted repo path with a leading `/`; POC-3 stage A §2's
|
//! single-quoted repo path with a leading `/`; POC-3 stage A §2's
|
||||||
//! verified grammar table):
|
//! verified grammar table):
|
||||||
//!
|
//!
|
||||||
//! - **`git-{upload,receive}-pack '<path>'`** → [`ExecRoute::Git`]:
|
//! - **`git-{upload,receive}-pack '<path>'`** → [`ExecRoute::Git`]:
|
||||||
//! the two serving services of ADR-016, one separating space, and a
|
//! the two serving services of ADR-016, one separating space, and a
|
||||||
//! path-shaped argument — quoted (the vanilla wire shape, fully
|
//! path-shaped argument: quoted (the vanilla wire shape, fully
|
||||||
//! single-quote-wrapped) or unquoted absolute (leading `/`). The
|
//! single-quote-wrapped) or unquoted absolute (leading `/`). The
|
||||||
//! path is normalized: quotes stripped, leading `/` stripped, and
|
//! path is normalized: quotes stripped, leading `/` stripped, and
|
||||||
//! **one trailing `.git` suffix stripped** — the vanilla-clone idiom
|
//! **one trailing `.git` suffix stripped** (the vanilla-clone idiom
|
||||||
//! (real git sends the remote URL's path verbatim, bare clones carry
|
//! (real git sends the remote URL's path verbatim, bare clones carry
|
||||||
//! `.git`; the alkgit registry grammar (`validate_repo_id`) excludes
|
//! `.git`; the alkgit registry grammar (`validate_repo_id`) excludes
|
||||||
//! `.`, so the door normalizes the idiom rather than asking
|
//! `.`, so the door normalizes the idiom rather than asking
|
||||||
//! registries for dotted keys; an empty remainder keeps `.git` and
|
//! registries for dotted keys; an empty remainder keeps `.git` and
|
||||||
//! fails validation — never silently emptied). The normalized path
|
//! fails validation, never silently emptied). The normalized path
|
||||||
//! is *not yet validated* — the git row (feature-gated) validates it
|
//! is *not yet validated*: the git row (feature-gated) validates it
|
||||||
//! against `alkgit::validate_repo_id` (1–2 segments, charset, ≤ 100
|
//! against `alkgit::validate_repo_id` (1–2 segments, charset, ≤ 100
|
||||||
//! bytes) and hands the id through; **alkssh never resolves** (the
|
//! bytes) and hands the id through; **alkssh never resolves** (the
|
||||||
//! no-existence-oracle posture lives in alkgit's collapsed generic
|
//! no-existence-oracle posture lives in alkgit's collapsed generic
|
||||||
//! denial, and an id failing the *shape* gate is the git row's
|
//! denial, and an id failing the *shape* gate is the git row's
|
||||||
//! `invalid-repo-id` decline — see below). Single-segment or
|
//! `invalid-repo-id` decline; see below). Single-segment or
|
||||||
//! `.git`-empty shapes ride the same grammar.
|
//! `.git`-empty shapes ride the same grammar.
|
||||||
//! - **`git-send-pack` / `git-upload-archive`** →
|
//! - **`git-send-pack` / `git-upload-archive`** →
|
||||||
//! [`ExecRoute::Declined`]([`ExecDecline::UnknownService`]): the
|
//! [`ExecRoute::Declined`]([`ExecDecline::UnknownService`]): the
|
||||||
//! known non-serving git services take the honest decline-with-reason
|
//! known non-serving git services take the honest decline-with-reason
|
||||||
//! surface (POC-3 §2; ADR-016's fixed refusal — `git-upload-archive`
|
//! surface (POC-3 §2; ADR-016's fixed refusal: `git-upload-archive`
|
||||||
//! is absent from serving by ADR-016 §1). Decision recorded
|
//! is absent from serving by ADR-016 §1). Decision recorded
|
||||||
//! (`w3-git-mapping`): they do **not** ride the pipe row; the decline
|
//! (`w3-git-mapping`): they do **not** ride the pipe row; the decline
|
||||||
//! is build-independent (no feature ever serves them).
|
//! is build-independent (no feature ever serves them).
|
||||||
//! - **A serving service with no argument at all** (service-only) →
|
//! - **A serving service with no argument at all** (service-only) →
|
||||||
//! [`ExecRoute::Declined`]([`ExecDecline::ServiceOnly`]): SSH exec
|
//! [`ExecRoute::Declined`]([`ExecDecline::ServiceOnly`]): SSH exec
|
||||||
//! must carry the repo (POC-3 §A — a subsystem-style name cannot
|
//! must carry the repo (POC-3 §A: a subsystem-style name cannot
|
||||||
//! dispatch, so exec without a repo cannot either).
|
//! dispatch, so exec without a repo cannot either).
|
||||||
//! - **Everything else** → [`ExecRoute::Pipe`] (parse-then-fail): not
|
//! - **Everything else** → [`ExecRoute::Pipe`] (parse-then-fail): not
|
||||||
//! git — including path-shaped-but-not-absolute unquoted arguments
|
//! git, including path-shaped-but-not-absolute unquoted arguments
|
||||||
//! (`git-upload-pack rel`, exact strings the wave-2 probe pinned)
|
//! (`git-upload-pack rel`, exact strings the wave-2 probe pinned)
|
||||||
//! and unterminated quotes; the probe's routing verdicts are
|
//! and unterminated quotes; the probe's routing verdicts are
|
||||||
//! preserved verbatim.
|
//! preserved verbatim.
|
||||||
//!
|
//!
|
||||||
//! ## The wire vocabulary (decision, recorded)
|
//! ## The wire vocabulary (decision, recorded)
|
||||||
//!
|
//!
|
||||||
//! SSH request replies carry no failure text, so — except for the open
|
//! SSH request replies carry no failure text, so, except for the open
|
||||||
//! level — the *reason* lives in the event vocabulary
|
//! level, the *reason* lives in the event vocabulary
|
||||||
//! ([`crate::observe`]), not on the wire. Decision: the grammar
|
//! ([`crate::observe`]), not on the wire. Decision: the grammar
|
||||||
//! declinables (`unknown-service`, `service-only`, and the git row's
|
//! declinables (`unknown-service`, `service-only`, and the git row's
|
||||||
//! `invalid-repo-id`) share the **request-level reply shape** with
|
//! `invalid-repo-id`) share the **request-level reply shape** with
|
||||||
//! parse-then-fail — `channel_failure`, channel left usable (the
|
//! parse-then-fail: `channel_failure`, channel left usable (the
|
||||||
//! POC-1 §5 session-survives-declines posture; POC-3's decline shape),
|
//! POC-1 §5 session-survives-declines posture; POC-3's decline shape),
|
||||||
//! distinguishable only in the events. The *serving-path* shape
|
//! distinguishable only in the events. The *serving-path* shape
|
||||||
//! (failure + close, the spawned task's) stays reserved for declined
|
//! (failure + close, the spawned task's) stays reserved for declined
|
||||||
//! rows that owned the channel: the seam-unconfigured git row and the
|
//! rows that owned the channel: the seam-unconfigured git row and the
|
||||||
//! real serve's own outcomes (the denial band alkgit writes before any
|
//! real serve's own outcomes (the denial band alkgit writes before any
|
||||||
//! protocol byte, then exit-status + close — the wire shapes the
|
//! protocol byte, then exit-status + close; the wire shapes the
|
||||||
//! `git_map` module pins).
|
//! `git_map` module pins).
|
||||||
|
|
||||||
use russh::ChannelOpenFailure;
|
use russh::ChannelOpenFailure;
|
||||||
@@ -80,7 +80,7 @@ use russh::ChannelOpenFailure;
|
|||||||
/// `w3-git-mapping` grammar with the declined-with-reason class).
|
/// `w3-git-mapping` grammar with the declined-with-reason class).
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ExecRoute {
|
pub enum ExecRoute {
|
||||||
/// A git-parseable command — routes to the git row. The repo path
|
/// A git-parseable command; it routes to the git row. The repo path
|
||||||
/// is the normalized argument grammar (quotes, leading `/`, one
|
/// is the normalized argument grammar (quotes, leading `/`, one
|
||||||
/// trailing `.git`); validation (`alkgit::validate_repo_id`) is
|
/// trailing `.git`); validation (`alkgit::validate_repo_id`) is
|
||||||
/// the git row's gate (the `crate::git_map` module).
|
/// the git row's gate (the `crate::git_map` module).
|
||||||
@@ -88,35 +88,35 @@ pub enum ExecRoute {
|
|||||||
service: GitServiceRoute,
|
service: GitServiceRoute,
|
||||||
repo: String,
|
repo: String,
|
||||||
},
|
},
|
||||||
/// Everything else — routes to the session pipe row (the
|
/// Everything else: routes to the session pipe row (the
|
||||||
/// `session` feature gate is the row's other half; unfeatured
|
/// `session` feature gate is the row's other half; unfeatured
|
||||||
/// builds decline here).
|
/// builds decline here).
|
||||||
Pipe,
|
Pipe,
|
||||||
/// A command the grammar recognizes as git-transport-shaped but
|
/// A command the grammar recognizes as git-transport-shaped but
|
||||||
/// that can never dispatch on this door — declined with a reason,
|
/// that can never dispatch on this door; it is declined with a reason,
|
||||||
/// honestly and build-independently (see the grammar section).
|
/// honestly and build-independently (see the grammar section).
|
||||||
Declined(ExecDecline),
|
Declined(ExecDecline),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The git service the exec grammar recognized — the ADR-016 wire
|
/// The git service the exec grammar recognized: the ADR-016 wire
|
||||||
/// strings, as the door-side form of `alkgit::Service` (the seam maps
|
/// strings, as the door-side form of `alkgit::Service` (the seam maps
|
||||||
/// them verbatim; this type keeps the parse core feature-free).
|
/// them verbatim; this type keeps the parse core feature-free).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum GitServiceRoute {
|
pub enum GitServiceRoute {
|
||||||
/// `git-upload-pack` — fetch.
|
/// `git-upload-pack`: fetch.
|
||||||
UploadPack,
|
UploadPack,
|
||||||
/// `git-receive-pack` — push.
|
/// `git-receive-pack`: push.
|
||||||
ReceivePack,
|
ReceivePack,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A git-shaped command that can never dispatch — the decline reasons
|
/// A git-shaped command that can never dispatch: the decline reasons
|
||||||
/// the grammar itself decides (the git row adds `invalid-repo-id` at
|
/// the grammar itself decides (the git row adds `invalid-repo-id` at
|
||||||
/// the validation gate; that one needs `alkgit`'s grammar, this type
|
/// the validation gate; that one needs `alkgit`'s grammar, this type
|
||||||
/// is the parse core's).
|
/// is the parse core's).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ExecDecline {
|
pub enum ExecDecline {
|
||||||
/// A known non-serving git service arrived on the wire:
|
/// A known non-serving git service arrived on the wire:
|
||||||
/// `git-send-pack` / `git-upload-archive` (POC-3 §2 — the honest
|
/// `git-send-pack` / `git-upload-archive` (POC-3 §2, the honest
|
||||||
/// surface; ADR-016's refusal).
|
/// surface; ADR-016's refusal).
|
||||||
UnknownService { service: &'static str },
|
UnknownService { service: &'static str },
|
||||||
/// A serving service name with no repo argument at all (POC-3
|
/// A serving service name with no repo argument at all (POC-3
|
||||||
@@ -126,7 +126,7 @@ pub enum ExecDecline {
|
|||||||
|
|
||||||
impl ExecDecline {
|
impl ExecDecline {
|
||||||
/// The event-vocabulary reason for this decline (§the wire
|
/// The event-vocabulary reason for this decline (§the wire
|
||||||
/// vocabulary — `&'static str` values only; no wire-carried
|
/// vocabulary: `&'static str` values only; no wire-carried
|
||||||
/// service token ever rides an event).
|
/// service token ever rides an event).
|
||||||
pub(crate) fn reason(self) -> &'static str {
|
pub(crate) fn reason(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
@@ -136,7 +136,7 @@ impl ExecDecline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The exec-grammar parse — the wave-2 `classify_exec` probe's
|
/// The exec-grammar parse: the wave-2 `classify_exec` probe's
|
||||||
/// replacement, its unit rows migrated (see the grammar section for
|
/// replacement, its unit rows migrated (see the grammar section for
|
||||||
/// the exact shape rules).
|
/// the exact shape rules).
|
||||||
pub fn parse_exec(command: &str) -> ExecRoute {
|
pub fn parse_exec(command: &str) -> ExecRoute {
|
||||||
@@ -174,13 +174,13 @@ pub fn parse_exec(command: &str) -> ExecRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The grammar's construction arm (`w4-op-rows`) — the client
|
/// The grammar's construction arm (`w4-op-rows`), the client
|
||||||
/// direction's mirror of [`parse_exec`]: the git route + repo id →
|
/// direction's mirror of [`parse_exec`]: the git route + repo id →
|
||||||
/// the vanilla wire command string, byte-for-byte the shape the
|
/// the vanilla wire command string, byte-for-byte the shape the
|
||||||
/// grammar parses back (`git-{upload,receive}-pack '/<repo>'` — the
|
/// grammar parses back (`git-{upload,receive}-pack '/<repo>'`, the
|
||||||
/// fully-single-quote-wrapped path with the leading `/`, POC-3 §A's
|
/// fully-single-quote-wrapped path with the leading `/`, POC-3 §A's
|
||||||
/// verified shape). The client row reuses — never forks — the
|
/// verified shape). The client row reuses the grammar, never forks
|
||||||
/// grammar: it hands the constructed command back through
|
/// it: it hands the constructed command back through
|
||||||
/// [`parse_exec`] before any wire op, so the shared verdicts (the
|
/// [`parse_exec`] before any wire op, so the shared verdicts (the
|
||||||
/// per-length cap, the decline classes) ride both arms
|
/// per-length cap, the decline classes) ride both arms
|
||||||
/// (`client.md` §op rows: "the same exec grammar the server side
|
/// (`client.md` §op rows: "the same exec grammar the server side
|
||||||
@@ -195,10 +195,10 @@ pub(crate) fn build_git_exec_command(service: GitServiceRoute, repo_id: &str) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The argument-grammar stage: a path-shaped argument (fully wrapped
|
/// The argument-grammar stage: a path-shaped argument (fully wrapped
|
||||||
/// in single quotes — the vanilla wire shape — or unquoted absolute;
|
/// in single quotes, the vanilla wire shape, or unquoted absolute;
|
||||||
/// anything else is not git) normalized to the repo path: quotes
|
/// anything else is not git) normalized to the repo path: quotes
|
||||||
/// stripped, leading `/` stripped, one trailing `.git` stripped (the
|
/// stripped, leading `/` stripped, one trailing `.git` stripped (the
|
||||||
/// recorded normalization — see the module grammar section; an
|
/// recorded normalization; see the module grammar section; an
|
||||||
/// empty-after-normalized id is *kept* for the git row's validation
|
/// empty-after-normalized id is *kept* for the git row's validation
|
||||||
/// to decline as `invalid-repo-id`, never silently emptied).
|
/// to decline as `invalid-repo-id`, never silently emptied).
|
||||||
fn normalize_repo_path(argument: &str) -> Option<String> {
|
fn normalize_repo_path(argument: &str) -> Option<String> {
|
||||||
@@ -222,10 +222,10 @@ fn normalize_repo_path(argument: &str) -> Option<String> {
|
|||||||
Some(normalized.to_owned())
|
Some(normalized.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The wire decline reason for a **featureless** row — a known channel
|
/// The wire decline reason for a **featureless** row: a known channel
|
||||||
/// type this build serves nothing of (unfeatured or never-feature):
|
/// type this build serves nothing of (unfeatured or never-feature):
|
||||||
/// `administratively prohibited`, the vanilla-observable policy
|
/// `administratively prohibited`, the vanilla-observable policy
|
||||||
/// refusal (POC-1 §5 — channel-scoped, auth intact, no hangs). A
|
/// refusal (POC-1 §5: channel-scoped, auth intact, no hangs). A
|
||||||
/// never-feature (`x11`, auth-agent) and an unfeatured feature row
|
/// never-feature (`x11`, auth-agent) and an unfeatured feature row
|
||||||
/// share this class honestly: without the feature there is no
|
/// share this class honestly: without the feature there is no
|
||||||
/// capability to configure, so nothing distinguishable exists.
|
/// capability to configure, so nothing distinguishable exists.
|
||||||
@@ -234,14 +234,14 @@ pub(crate) fn unfeatured_open_decline() -> ChannelOpenFailure {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The wire decline reason for a **cannot-bridge** tunnel open
|
/// The wire decline reason for a **cannot-bridge** tunnel open
|
||||||
/// (`w3-tunnel-direct` — the wave-2 seam stub's reason surviving as
|
/// (`w3-tunnel-direct`, the wave-2 seam stub's reason surviving as
|
||||||
/// the real decline): `connect failed`, the explicit reject shape
|
/// the real decline): `connect failed`, the explicit reject shape
|
||||||
/// ADR-006 decision 1 pins — accept only after the fabric open
|
/// ADR-006 decision 1 pins: accept only after the fabric open
|
||||||
/// succeeds, and a fabric-open failure maps to this explicit `SSH
|
/// succeeds, and a fabric-open failure maps to this explicit `SSH
|
||||||
/// `ConnectFailed` (the channel never establishes, so the client
|
/// `ConnectFailed` (the channel never establishes, so the client
|
||||||
/// observes a channel-scoped refusal, no hang). The same code covers
|
/// observes a channel-scoped refusal, no hang). The same code covers
|
||||||
/// the other cannot-bridge classes of the direct rows (the fabric
|
/// the other cannot-bridge classes of the direct rows (the fabric
|
||||||
/// entry unconfigured under the feature — the unset-declines posture —
|
/// entry unconfigured under the feature (the unset-declines posture),
|
||||||
/// and wire arguments naming an impossible target; a vanilla client
|
/// and wire arguments naming an impossible target; a vanilla client
|
||||||
/// cannot distinguish these from a dead target, and the event
|
/// cannot distinguish these from a dead target, and the event
|
||||||
/// vocabulary records which). Policy refusals stay
|
/// vocabulary records which). Policy refusals stay
|
||||||
@@ -252,7 +252,7 @@ pub(crate) fn connect_failed_open_decline() -> ChannelOpenFailure {
|
|||||||
|
|
||||||
/// The wire decline reason for an open beyond the **in-crate
|
/// The wire decline reason for an open beyond the **in-crate
|
||||||
/// concurrent-channel cap** (`overview.md` §Bounded-resources policy,
|
/// concurrent-channel cap** (`overview.md` §Bounded-resources policy,
|
||||||
/// `w2-bounded-resources`): `administratively prohibited` — the cap is
|
/// `w2-bounded-resources`): `administratively prohibited`; the cap is
|
||||||
/// the assembly's own configured policy ceiling, refused fail-closed
|
/// the assembly's own configured policy ceiling, refused fail-closed
|
||||||
/// at the open (never queued, never half-open). OpenSSH's own
|
/// at the open (never queued, never half-open). OpenSSH's own
|
||||||
/// `MaxSessions` ceiling answers the same code, so vanilla clients
|
/// `MaxSessions` ceiling answers the same code, so vanilla clients
|
||||||
@@ -281,8 +281,8 @@ mod tests {
|
|||||||
git(command).1
|
git(command).1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* ADR-006 decision 2 (the exec-only git parse
|
/// - *Contract stamp:* ADR-006 decision 2 (the exec-only git parse:
|
||||||
/// — the vanilla wire shape `git-upload-pack '/owner/demo'`).
|
/// the vanilla wire shape `git-upload-pack '/owner/demo'`).
|
||||||
#[test]
|
#[test]
|
||||||
fn vanilla_quoted_upload_pack_parse_is_git() {
|
fn vanilla_quoted_upload_pack_parse_is_git() {
|
||||||
let (service, repo) = git("git-upload-pack '/owner/demo'");
|
let (service, repo) = git("git-upload-pack '/owner/demo'");
|
||||||
@@ -316,16 +316,16 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
git_repo("git-upload-pack '/r.git.git'"),
|
git_repo("git-upload-pack '/r.git.git'"),
|
||||||
"r.git",
|
"r.git",
|
||||||
"one suffix only — the remainder fails validation honestly"
|
"one suffix only; the remainder fails validation honestly"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
git_repo("git-upload-pack '/.git'"),
|
git_repo("git-upload-pack '/.git'"),
|
||||||
".git",
|
".git",
|
||||||
"the empty remainder is kept — the git row declines it, never a silent emptying"
|
"the empty remainder is kept (the git row declines it, never a silent emptying)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* POC-3 §2 (service-only → declined — SSH
|
/// - *Contract stamp:* POC-3 §2 (service-only → declined: SSH
|
||||||
/// exec must carry the repo; the decline-with-reason surface,
|
/// exec must carry the repo; the decline-with-reason surface,
|
||||||
/// not the pipe row).
|
/// not the pipe row).
|
||||||
#[test]
|
#[test]
|
||||||
@@ -341,7 +341,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* POC-3 §2 (unknown services declined
|
/// - *Contract stamp:* POC-3 §2 (unknown services declined
|
||||||
/// honestly — the decision that they take the decline surface,
|
/// honestly; the decision that they take the decline surface,
|
||||||
/// not the pipe row).
|
/// not the pipe row).
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_services_parse_to_the_decline() {
|
fn unknown_services_parse_to_the_decline() {
|
||||||
@@ -360,7 +360,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* mapping.md §Session-channel dispatch
|
/// - *Contract stamp:* mapping.md §Session-channel dispatch
|
||||||
/// (non-git → parse-then-fail, the pipe row — the wave-2 probe's
|
/// (non-git → parse-then-fail, the pipe row; the wave-2 probe's
|
||||||
/// pinned routing verdicts, preserved).
|
/// pinned routing verdicts, preserved).
|
||||||
#[test]
|
#[test]
|
||||||
fn non_git_commands_parse_to_the_pipe_row() {
|
fn non_git_commands_parse_to_the_pipe_row() {
|
||||||
@@ -378,7 +378,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unterminated empty quotes are not the vanilla path shape — the
|
/// Unterminated empty quotes are not the vanilla path shape; the
|
||||||
/// probe's pipe verdict applies. A quoted path without the leading
|
/// probe's pipe verdict applies. A quoted path without the leading
|
||||||
/// slash is still path-shape (the POC's loose quote trim); it rides
|
/// slash is still path-shape (the POC's loose quote trim); it rides
|
||||||
/// the grammar to the git row's validation.
|
/// the grammar to the git row's validation.
|
||||||
@@ -390,7 +390,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Quoted-empty and slash-only arguments parse as git with an empty
|
/// Quoted-empty and slash-only arguments parse as git with an empty
|
||||||
/// normalized id — the git row's validation declines them as
|
/// normalized id; the git row's validation declines them as
|
||||||
/// `invalid-repo-id` (kept, never silently emptied).
|
/// `invalid-repo-id` (kept, never silently emptied).
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_ids_parse_git_and_the_row_downstream_declines() {
|
fn empty_ids_parse_git_and_the_row_downstream_declines() {
|
||||||
@@ -399,7 +399,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* mapping.md §Session-channel dispatch (deep
|
/// - *Contract stamp:* mapping.md §Session-channel dispatch (deep
|
||||||
/// paths ride the grammar to the git row — validation is the
|
/// paths ride the grammar to the git row; validation is the
|
||||||
/// registry-grammar gate downstream, e.g. 3 segments decline as
|
/// registry-grammar gate downstream, e.g. 3 segments decline as
|
||||||
/// `invalid-repo-id`, not at the parse).
|
/// `invalid-repo-id`, not at the parse).
|
||||||
#[test]
|
#[test]
|
||||||
@@ -418,7 +418,7 @@ mod tests {
|
|||||||
|
|
||||||
/// - *Contract stamp:* `client.md` §op rows (the same exec
|
/// - *Contract stamp:* `client.md` §op rows (the same exec
|
||||||
/// grammar the server side parses, reused not forked);
|
/// grammar the server side parses, reused not forked);
|
||||||
/// ADR-006 decision 2 — the construction arm's command parses
|
/// ADR-006 decision 2: the construction arm's command parses
|
||||||
/// back through [`parse_exec`] to exactly the route it was
|
/// back through [`parse_exec`] to exactly the route it was
|
||||||
/// built from, for both services and every grammar shape.
|
/// built from, for both services and every grammar shape.
|
||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
@@ -451,7 +451,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the construction arm is grammar-loyal —
|
/// - *Contract stamp:* the construction arm is grammar-loyal:
|
||||||
/// it is only ever reachable for the two *serving* services
|
/// it is only ever reachable for the two *serving* services
|
||||||
/// (the type is the gate); the wire shape is fully
|
/// (the type is the gate); the wire shape is fully
|
||||||
/// single-quote-wrapped with the leading `/`.
|
/// single-quote-wrapped with the leading `/`.
|
||||||
|
|||||||
+65
-65
@@ -1,25 +1,25 @@
|
|||||||
//! The crate's error taxonomy — fail-closed, no catch-alls (ADR-005
|
//! The crate's error taxonomy: fail-closed, no catch-alls (ADR-005
|
||||||
//! decision 4: the vault's error taxonomy is the adapter's to reshape,
|
//! decision 4: the vault's error taxonomy is the adapter's to reshape,
|
||||||
//! never to collapse).
|
//! never to collapse).
|
||||||
//!
|
//!
|
||||||
//! Deliberately **not** error types, per the family contracts:
|
//! Deliberately **not** error types, per the family contracts:
|
||||||
//!
|
//!
|
||||||
//! - **Auth declines** — dispositions (`Auth::Reject`, ADR-004),
|
//! - **Auth declines**: dispositions (`Auth::Reject`, ADR-004),
|
||||||
//! never errors; a decline is an authorized outcome, not a failure.
|
//! never errors; a decline is an authorized outcome, not a failure.
|
||||||
//! - **Wire decline reasons** — SSH-shaped responses to the client
|
//! - **Wire decline reasons**: SSH-shaped responses to the client
|
||||||
//! (the wave-2 decline core's material), never this taxonomy.
|
//! (the wave-2 decline core's material), never this taxonomy.
|
||||||
//! - **Config validation** — the OQ-08/serve-config fields are
|
//! - **Config validation**: the OQ-08/serve-config fields are
|
||||||
//! `Option` and decline at request time, not construction time; no
|
//! `Option` and decline at request time, not construction time; no
|
||||||
//! config-error type exists unless a need is found.
|
//! config-error type exists unless a need is found.
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Every way a vault-backed key source can fail to produce key
|
/// Every way a vault-backed key source can fail to produce key
|
||||||
/// material (ADR-005 decision 4 — the failure matrix as distinct
|
/// material (ADR-005 decision 4: the failure matrix as distinct
|
||||||
/// variants). All paths are fail-closed: a failed load never yields a
|
/// variants). All paths are fail-closed: a failed load never yields a
|
||||||
/// dial, and a failed host-key load never yields a server.
|
/// dial, and a failed host-key load never yields a server.
|
||||||
///
|
///
|
||||||
/// The vault itself is a secret store on this path — the error
|
/// The vault itself is a secret store on this path; the error
|
||||||
/// taxonomy here is the adapter's reshaping of the vault's, never a
|
/// taxonomy here is the adapter's reshaping of the vault's, never a
|
||||||
/// re-export of it. Variants are deliberately closed: no catch-all,
|
/// re-export of it. Variants are deliberately closed: no catch-all,
|
||||||
/// no `Other`, no `Unknown`.
|
/// no `Other`, no `Unknown`.
|
||||||
@@ -28,14 +28,14 @@ pub enum KeySourceError {
|
|||||||
/// The vault was locked when the key was requested.
|
/// The vault was locked when the key was requested.
|
||||||
///
|
///
|
||||||
/// ADR-005 semantics: the vault participates only at use time
|
/// ADR-005 semantics: the vault participates only at use time
|
||||||
/// (decrypt-at-use), so a locked vault blocks exactly that — no
|
/// (decrypt-at-use), so a locked vault blocks exactly that: no
|
||||||
/// key material can be decrypted, so there is no dial; on the
|
/// key material can be decrypted, so there is no dial; on the
|
||||||
/// host-key path the load feeds server construction
|
/// host-key path the load feeds server construction
|
||||||
/// (decrypt-at-construction), so no server is constructed
|
/// (decrypt-at-construction), so no server is constructed
|
||||||
/// (POC-2 §5). Live sessions are unaffected: the decoded
|
/// (POC-2 §5). Live sessions are unaffected: the decoded
|
||||||
/// `PrivateKey` is a dial's lifetime, not a vault-coupled lease.
|
/// `PrivateKey` is a dial's lifetime, not a vault-coupled lease.
|
||||||
///
|
///
|
||||||
/// Fail-closed consequence: operation refused. Never unlock —
|
/// Fail-closed consequence: operation refused. Never unlock;
|
||||||
/// the vault's lifecycle is the assembly's, never the adapter's.
|
/// the vault's lifecycle is the assembly's, never the adapter's.
|
||||||
#[error("vault is locked; no key material available until unlocked")]
|
#[error("vault is locked; no key material available until unlocked")]
|
||||||
Locked,
|
Locked,
|
||||||
@@ -43,7 +43,7 @@ pub enum KeySourceError {
|
|||||||
/// No blob exists at the requested key location.
|
/// No blob exists at the requested key location.
|
||||||
///
|
///
|
||||||
/// ADR-005 semantics: the assembly source has no
|
/// ADR-005 semantics: the assembly source has no
|
||||||
/// `EncryptedData` record for the requested key — no vault call
|
/// `EncryptedData` record for the requested key, so no vault call
|
||||||
/// is needed to know (POC-2 §3). Distinct from every other
|
/// is needed to know (POC-2 §3). Distinct from every other
|
||||||
/// variant on purpose: an absent key is a configuration/
|
/// variant on purpose: an absent key is a configuration/
|
||||||
/// registration gap, while a locked/decryption failure is a
|
/// registration gap, while a locked/decryption failure is a
|
||||||
@@ -55,11 +55,11 @@ pub enum KeySourceError {
|
|||||||
#[error("no key material at the requested key location")]
|
#[error("no key material at the requested key location")]
|
||||||
Absent,
|
Absent,
|
||||||
|
|
||||||
/// Decryption failed — wrong mnemonic or tampered ciphertext.
|
/// Decryption failed: wrong mnemonic or tampered ciphertext.
|
||||||
///
|
///
|
||||||
/// ADR-005 semantics: the AES-256-GCM open failed (POC-2 §3):
|
/// ADR-005 semantics: the AES-256-GCM open failed (POC-2 §3):
|
||||||
/// the mnemonic derives an unrelated key, or the ciphertext was
|
/// the mnemonic derives an unrelated key, or the ciphertext was
|
||||||
/// altered in flight — the refuse-at-crypto class. The vault
|
/// altered in flight (the refuse-at-crypto class). The vault
|
||||||
/// reports this class as opaque message text, which is the
|
/// reports this class as opaque message text, which is the
|
||||||
/// honest content here.
|
/// honest content here.
|
||||||
///
|
///
|
||||||
@@ -71,31 +71,31 @@ pub enum KeySourceError {
|
|||||||
/// The record's key version is one the vault cannot resolve.
|
/// The record's key version is one the vault cannot resolve.
|
||||||
///
|
///
|
||||||
/// ADR-005 semantics: the vault declines *before* touching
|
/// ADR-005 semantics: the vault declines *before* touching
|
||||||
/// crypto — an unknown/unresolvable `key_version` (reserved/legacy
|
/// crypto: an unknown/unresolvable `key_version` (reserved/legacy
|
||||||
/// v1 is the TS_PBKDF2 class; a future version the vault predates
|
/// v1 is the TS_PBKDF2 class; a future version the vault predates
|
||||||
/// is the other class) is refused at derivation-path resolution
|
/// is the other class) is refused at derivation-path resolution
|
||||||
/// (POC-2 §3). `EncryptedData` remains opaque: the crate never
|
/// (POC-2 §3). `EncryptedData` remains opaque: the crate never
|
||||||
/// interprets versions — this variant carries only the vault's
|
/// interprets versions; this variant carries only the vault's
|
||||||
/// refusal message.
|
/// refusal message.
|
||||||
///
|
///
|
||||||
/// Fail-closed consequence: no dial; on the host-key path, no
|
/// Fail-closed consequence: no dial; on the host-key path, no
|
||||||
/// server construction. Rotation (higher version the vault
|
/// server construction. Rotation (higher version the vault
|
||||||
/// supports) is the storage-side migration — a version the
|
/// supports) is the storage-side migration; a version the
|
||||||
/// vault cannot derive never becomes a partially usable key.
|
/// vault cannot derive never becomes a partially usable key.
|
||||||
#[error("vault cannot resolve the record's version: {0}")]
|
#[error("vault cannot resolve the record's version: {0}")]
|
||||||
UnknownKeyVersion(String),
|
UnknownKeyVersion(String),
|
||||||
|
|
||||||
/// The decrypted plaintext arrived but is not decodable key
|
/// The decrypted plaintext arrived but is not decodable key
|
||||||
/// material — the one-time [`decode_secret_key`](russh::keys::decode_secret_key)
|
/// material, that is, the one-time [`decode_secret_key`](russh::keys::decode_secret_key)
|
||||||
/// failure.
|
/// failure.
|
||||||
///
|
///
|
||||||
/// ADR-005 semantics: the vault decrypted *something*
|
/// ADR-005 semantics: the vault decrypted *something*
|
||||||
/// successfully and the plaintext is not an OpenSSH
|
/// successfully and the plaintext is not an OpenSSH
|
||||||
/// private-key PEM (decision 1's inner format). Distinct from
|
/// private-key PEM (decision 1's inner format). Distinct from
|
||||||
/// [`KeySourceError::Decryption`] on purpose — the
|
/// [`KeySourceError::Decryption`] on purpose: the
|
||||||
/// custody boundary held, what was guarded is not a key. The
|
/// custody boundary held, what was guarded is not a key. The
|
||||||
/// underlying parse error's message is deliberately *not*
|
/// underlying parse error's message is deliberately *not*
|
||||||
/// carried — the machinery's text can echo plaintext-shaped
|
/// carried, because the machinery's text can echo plaintext-shaped
|
||||||
/// material (the `w5-fuzz-harness` finding: the `ssh-key` PPK
|
/// material (the `w5-fuzz-harness` finding: the `ssh-key` PPK
|
||||||
/// path interpolates the raw header), so the payload is
|
/// path interpolates the raw header), so the payload is
|
||||||
/// crate-owned fixed text; the RC-pinned `ssh-key` error type
|
/// crate-owned fixed text; the RC-pinned `ssh-key` error type
|
||||||
@@ -107,24 +107,24 @@ pub enum KeySourceError {
|
|||||||
Decode(String),
|
Decode(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The adapter (server-direction) error surface — the failure classes
|
/// The adapter (server-direction) error surface, the failure classes
|
||||||
/// the mount point can reach, deliberately closed:
|
/// the mount point can reach, deliberately closed:
|
||||||
///
|
///
|
||||||
/// - **Construction** — the [`SshAdapter::new`](crate::SshAdapter::new)
|
/// - **Construction**: the [`SshAdapter::new`](crate::SshAdapter::new)
|
||||||
/// setup failed *before any session exists*: a failed host-key load
|
/// setup failed *before any session exists*: a failed host-key load
|
||||||
/// (the [`KeySourceError`] variant is preserved verbatim — locked /
|
/// (the [`KeySourceError`] variant is preserved verbatim: locked /
|
||||||
/// absent / decryption / unknown-version / decode each stays itself)
|
/// absent / decryption / unknown-version / decode each stays itself)
|
||||||
/// or a refused `-R` listen-leg registration (`ListenRegistration`,
|
/// or a refused `-R` listen-leg registration (`ListenRegistration`,
|
||||||
/// `w3-tunnel-listen` — a fabric that could mint listens the door
|
/// `w3-tunnel-listen`; a fabric that could mint listens the door
|
||||||
/// cannot serve is worse than a half-configured adapter).
|
/// cannot serve is worse than a half-configured adapter).
|
||||||
/// **No adapter is constructed** (ADR-005 decision 6 — never a
|
/// **No adapter is constructed** (ADR-005 decision 6: never a
|
||||||
/// half-configured adapter, never a keyless server that fails
|
/// half-configured adapter, never a keyless server that fails
|
||||||
/// later).
|
/// later).
|
||||||
/// - **Wire** — the russh session stack's own error, mapped
|
/// - **Wire**: the russh session stack's own error, mapped
|
||||||
/// transparently (`From<russh::Error>`, the wave-1 error-model's
|
/// transparently (`From<russh::Error>`, the wave-1 error-model's
|
||||||
/// reserved `From` requirement of the per-connection
|
/// reserved `From` requirement of the per-connection
|
||||||
/// [`russh::server::Handler`]): KEX, transport, and IO failures of
|
/// [`russh::server::Handler`]): KEX, transport, and IO failures of
|
||||||
/// a mounted session. Auth declines are *not* this class — they are
|
/// a mounted session. Auth declines are *not* this class; they are
|
||||||
/// dispositions (ADR-004), and wire decline reasons are the
|
/// dispositions (ADR-004), and wire decline reasons are the
|
||||||
/// dispatch's SSH-shaped responses, never this taxonomy.
|
/// dispatch's SSH-shaped responses, never this taxonomy.
|
||||||
///
|
///
|
||||||
@@ -132,7 +132,7 @@ pub enum KeySourceError {
|
|||||||
/// direction can fail is one of these classes or a decline.
|
/// direction can fail is one of these classes or a decline.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum AdapterError {
|
pub enum AdapterError {
|
||||||
/// The host-key load at adapter construction refused — fail-closed
|
/// The host-key load at adapter construction refused, fail-closed
|
||||||
/// (ADR-005 decision 6). The [`KeySourceError`] variant is the
|
/// (ADR-005 decision 6). The [`KeySourceError`] variant is the
|
||||||
/// original refusal class, preserved un-collapsed.
|
/// original refusal class, preserved un-collapsed.
|
||||||
#[error("ssh server construction refused: {0}")]
|
#[error("ssh server construction refused: {0}")]
|
||||||
@@ -140,14 +140,14 @@ pub enum AdapterError {
|
|||||||
|
|
||||||
/// The `-R` listen leg's door-side openable registration refused
|
/// The `-R` listen leg's door-side openable registration refused
|
||||||
/// at construction (`w3-tunnel-listen`): fail-closed, never a
|
/// at construction (`w3-tunnel-listen`): fail-closed, never a
|
||||||
/// half-configured adapter — a construction where the fabric could
|
/// half-configured adapter: a construction where the fabric could
|
||||||
/// mint listens the door cannot serve is worse than no adapter.
|
/// mint listens the door cannot serve is worse than no adapter.
|
||||||
/// The string carries the registration failure detail.
|
/// The string carries the registration failure detail.
|
||||||
///
|
///
|
||||||
/// This stays the construction-time class: the wire-time sibling
|
/// This stays the construction-time class: the wire-time sibling
|
||||||
/// (`w4-router-collision`, the `ListenRouter::register`
|
/// (`w4-router-collision`, the `ListenRouter::register`
|
||||||
/// token-collision refusal) is a request-time **decline
|
/// token-collision refusal) is a request-time **decline
|
||||||
/// disposition** (`RequestDenied`, reason `token-collision`) —
|
/// disposition** (`RequestDenied`, reason `token-collision`);
|
||||||
/// a global-request hook's `Err` (this taxonomy's only route into
|
/// a global-request hook's `Err` (this taxonomy's only route into
|
||||||
/// the request surface) would kill the whole russh session, and
|
/// the request surface) would kill the whole russh session, and
|
||||||
/// wire decline reasons are never this taxonomy.
|
/// wire decline reasons are never this taxonomy.
|
||||||
@@ -160,53 +160,53 @@ pub enum AdapterError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The dial surface's failure classes (the client direction's error
|
/// The dial surface's failure classes (the client direction's error
|
||||||
/// counterpart of [`AdapterError`]) — deliberately closed, every
|
/// counterpart of [`AdapterError`]), deliberately closed, every
|
||||||
/// variant a no-connection outcome (ADR-005 decision 4: failures are
|
/// variant a no-connection outcome (ADR-005 decision 4: failures are
|
||||||
/// fail-closed and distinct, never collapsed, never a fallback dial):
|
/// fail-closed and distinct, never collapsed, never a fallback dial):
|
||||||
///
|
///
|
||||||
/// - **KeySource** — the decrypt-at-dial load refused. The
|
/// - **KeySource**: the decrypt-at-dial load refused. The
|
||||||
/// [`KeySourceError`] variant is preserved verbatim (locked / absent
|
/// [`KeySourceError`] variant is preserved verbatim (locked / absent
|
||||||
/// / decryption / unknown-version / decode each stays itself — the
|
/// / decryption / unknown-version / decode each stays itself, the
|
||||||
/// ADR-005 decision 4 matrix read through the dial seam). No key
|
/// ADR-005 decision 4 matrix read through the dial seam). No key
|
||||||
/// material, no dial.
|
/// material, no dial.
|
||||||
/// - **Address** — the dial target is not a parseable `host:port`.
|
/// - **Address**: the dial target is not a parseable `host:port`.
|
||||||
/// Checked at dial time (the config carries no construction-time
|
/// Checked at dial time (the config carries no construction-time
|
||||||
/// validation, the family contract); a malformed target is a
|
/// validation, the family contract); a malformed target is a
|
||||||
/// refusal before any connection.
|
/// refusal before any connection.
|
||||||
/// - **HostKeyRejected** — the injected host-key policy
|
/// - **HostKeyRejected**: the injected host-key policy
|
||||||
/// ([`crate::HostKeyPolicy`]) declined the presented server key at
|
/// ([`crate::HostKeyPolicy`]) declined the presented server key at
|
||||||
/// `check_server_key` (a `false` verdict is the decline
|
/// `check_server_key` (a `false` verdict is the decline
|
||||||
/// disposition; russh surfaces it as its `UnknownKey` wire error,
|
/// disposition; russh surfaces it as its `UnknownKey` wire error,
|
||||||
/// which this crate maps here because that hook is the only
|
/// which this crate maps here because that hook is the only
|
||||||
/// producer). The trust decision is the policy's — its own record
|
/// producer). The trust decision is the policy's; its own record
|
||||||
/// (its decision log) carries the reasons; the crate never
|
/// (its decision log) carries the reasons; the crate never
|
||||||
/// second-guesses or retries.
|
/// second-guesses or retries.
|
||||||
/// - **AuthRejected** — the remote end refused the publickey
|
/// - **AuthRejected**: the remote end refused the publickey
|
||||||
/// authentication (the family contract is exactly one
|
/// authentication (the family contract is exactly one
|
||||||
/// publickey-only attempt; there is no retry, no fallback method).
|
/// publickey-only attempt; there is no retry, no fallback method).
|
||||||
/// - **Wire** — the russh session stack's own error, mapped
|
/// - **Wire**: the russh session stack's own error, mapped
|
||||||
/// transparently, including the dial's connect/resolve/IO classes.
|
/// transparently, including the dial's connect/resolve/IO classes.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum DialError {
|
pub enum DialError {
|
||||||
/// The dial key's decrypt-at-dial load refused — every
|
/// The dial key's decrypt-at-dial load refused; every
|
||||||
/// [`KeySourceError`] variant is a no-dial outcome, preserved
|
/// [`KeySourceError`] variant is a no-dial outcome, preserved
|
||||||
/// un-collapsed (ADR-005 decision 4).
|
/// un-collapsed (ADR-005 decision 4).
|
||||||
#[error("dial key source refused: {0}")]
|
#[error("dial key source refused: {0}")]
|
||||||
KeySource(#[from] KeySourceError),
|
KeySource(#[from] KeySourceError),
|
||||||
|
|
||||||
/// The dial target is not a parseable `host:port` address — the
|
/// The dial target is not a parseable `host:port` address, so the
|
||||||
/// wire never dialed. The string carries the address as given.
|
/// wire never dialed. The string carries the address as given.
|
||||||
#[error("dial target is not a usable host:port address: {0}")]
|
#[error("dial target is not a usable host:port address: {0}")]
|
||||||
Address(String),
|
Address(String),
|
||||||
|
|
||||||
/// The host-key policy declined the presented server key — the
|
/// The host-key policy declined the presented server key, so the
|
||||||
/// dial aborts (a disposition the dial cannot proceed on; the
|
/// dial aborts (a disposition the dial cannot proceed on; the
|
||||||
/// crate default [`RejectAll`](crate::RejectAll) policy makes
|
/// crate default [`RejectAll`](crate::RejectAll) policy makes
|
||||||
/// every dial refuse, by design).
|
/// every dial refuse, by design).
|
||||||
#[error("host-key policy declined the presented server key")]
|
#[error("host-key policy declined the presented server key")]
|
||||||
HostKeyRejected,
|
HostKeyRejected,
|
||||||
|
|
||||||
/// The remote end refused the publickey authentication — the
|
/// The remote end refused the publickey authentication; this is
|
||||||
/// family's one-attempt publickey-only posture (no retries, no
|
/// family's one-attempt publickey-only posture (no retries, no
|
||||||
/// fallback methods, never another key).
|
/// fallback methods, never another key).
|
||||||
#[error("authentication was refused by the remote end")]
|
#[error("authentication was refused by the remote end")]
|
||||||
@@ -224,7 +224,7 @@ impl From<russh::Error> for DialError {
|
|||||||
/// The transparent russh mapping with one semantic upgrade:
|
/// The transparent russh mapping with one semantic upgrade:
|
||||||
/// `UnknownKey` is the wire shape of a host-key policy decline
|
/// `UnknownKey` is the wire shape of a host-key policy decline
|
||||||
/// (the client's `check_server_key` hook is the only producer),
|
/// (the client's `check_server_key` hook is the only producer),
|
||||||
/// so it becomes [`DialError::HostKeyRejected`] — every other
|
/// so it becomes [`DialError::HostKeyRejected`]; every other
|
||||||
/// error stays the transparent [`DialError::Wire`] class.
|
/// error stays the transparent [`DialError::Wire`] class.
|
||||||
fn from(error: russh::Error) -> Self {
|
fn from(error: russh::Error) -> Self {
|
||||||
match error {
|
match error {
|
||||||
@@ -235,22 +235,22 @@ impl From<russh::Error> for DialError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The family-typed mirror of the SSH `CHANNEL_OPEN` rejection
|
/// The family-typed mirror of the SSH `CHANNEL_OPEN` rejection
|
||||||
/// reasons (RFC 4250 §1.5 / RFC 4254 §5.2) — the op rows translate
|
/// reasons (RFC 4250 §1.5 / RFC 4254 §5.2); the op rows translate
|
||||||
/// the wire's typed-open decline into this closed vocabulary
|
/// the wire's typed-open decline into this closed vocabulary
|
||||||
/// (`client.md` §op rows: the family surface carries no vanilla types).
|
/// (`client.md` §op rows: the family surface carries no vanilla types).
|
||||||
/// The mapping is one-to-one with russh's
|
/// The mapping is one-to-one with russh's
|
||||||
/// [`russh::ChannelOpenFailure`], whose `Other` arm mirrors its
|
/// [`russh::ChannelOpenFailure`], whose `Other` arm mirrors its
|
||||||
/// `code`/`reason` pair verbatim — a decline reason the client
|
/// `code`/`reason` pair verbatim, so a decline reason the client
|
||||||
/// *could not map* is never silently collapsed onto a known class.
|
/// *could not map* is never silently collapsed onto a known class.
|
||||||
#[derive(Debug, Clone, Error, PartialEq, Eq)]
|
#[derive(Debug, Clone, Error, PartialEq, Eq)]
|
||||||
pub enum OpenDeclineReason {
|
pub enum OpenDeclineReason {
|
||||||
/// The SSH `administratively prohibited` class — a policy
|
/// The SSH `administratively prohibited` class: a policy
|
||||||
/// refusal at the server (the door's own unfeatured/cap
|
/// refusal at the server (the door's own unfeatured/cap
|
||||||
/// declines answer with this family's server-direction mirror).
|
/// declines answer with this family's server-direction mirror).
|
||||||
#[error("the server refused the open (administratively prohibited)")]
|
#[error("the server refused the open (administratively prohibited)")]
|
||||||
Prohibited,
|
Prohibited,
|
||||||
|
|
||||||
/// The SSH `connect failed` class — the target could not be
|
/// The SSH `connect failed` class: the target could not be
|
||||||
/// bridged (a dead `-L` target, an impossible one).
|
/// bridged (a dead `-L` target, an impossible one).
|
||||||
#[error("the server rejected the open (connect failed)")]
|
#[error("the server rejected the open (connect failed)")]
|
||||||
ConnectFailed,
|
ConnectFailed,
|
||||||
@@ -263,7 +263,7 @@ pub enum OpenDeclineReason {
|
|||||||
#[error("the server rejected the open (resource shortage)")]
|
#[error("the server rejected the open (resource shortage)")]
|
||||||
ResourceShortage,
|
ResourceShortage,
|
||||||
|
|
||||||
/// A rejection reason with no standard code — the wire's own
|
/// A rejection reason with no standard code: the wire's own
|
||||||
/// `code`/`reason` pair rides (the mirror never invents a class
|
/// `code`/`reason` pair rides (the mirror never invents a class
|
||||||
/// for an unmappable one).
|
/// for an unmappable one).
|
||||||
#[error("the server rejected the open (code {code}: {reason})")]
|
#[error("the server rejected the open (code {code}: {reason})")]
|
||||||
@@ -283,12 +283,12 @@ impl From<russh::ChannelOpenFailure> for OpenDeclineReason {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The client direction's op-row failure classes (`w4-op-rows`) —
|
/// The client direction's op-row failure classes (`w4-op-rows`),
|
||||||
/// the exec/pty / tunnel / git-pack op surfaces' closed taxonomy
|
/// the exec/pty / tunnel / git-pack op surfaces' closed taxonomy
|
||||||
/// (`RemoteSession` / `TunnelChannel` / `GitPackSession` — the
|
/// (`RemoteSession` / `TunnelChannel` / `GitPackSession`, the
|
||||||
/// feature-gated members live in the crate root's exports), deliberately distinct from [`DialError`]:
|
/// feature-gated members live in the crate root's exports), deliberately distinct from [`DialError`]:
|
||||||
/// an op runs on an *already-dialled* connection, so its failure is
|
/// an op runs on an *already-dialled* connection, so its failure is
|
||||||
/// never a no-connection outcome — the connection stays usable
|
/// never a no-connection outcome: the connection stays usable
|
||||||
/// ([`OpError::OpenDeclined`] / [`OpError::RequestRefused`] and
|
/// ([`OpError::OpenDeclined`] / [`OpError::RequestRefused`] and
|
||||||
/// [`OpError::ChannelEnded`] are channel/op-scoped, mirroring the
|
/// [`OpError::ChannelEnded`] are channel/op-scoped, mirroring the
|
||||||
/// server direction's decline shapes: channel-scoped, auth intact,
|
/// server direction's decline shapes: channel-scoped, auth intact,
|
||||||
@@ -299,7 +299,7 @@ impl From<russh::ChannelOpenFailure> for OpenDeclineReason {
|
|||||||
///
|
///
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum OpError {
|
pub enum OpError {
|
||||||
/// The server refused the op's **channel open** — the typed
|
/// The server refused the op's **channel open**, the typed
|
||||||
/// open's decline (POC-1 §6: a rejected open resolves on the
|
/// open's decline (POC-1 §6: a rejected open resolves on the
|
||||||
/// awaiting call, never a `wait()` match). The family-mirrored
|
/// awaiting call, never a `wait()` match). The family-mirrored
|
||||||
/// reason rides; the connection stays usable (row-pinned).
|
/// reason rides; the connection stays usable (row-pinned).
|
||||||
@@ -307,7 +307,7 @@ pub enum OpError {
|
|||||||
OpenDeclined(#[from] OpenDeclineReason),
|
OpenDeclined(#[from] OpenDeclineReason),
|
||||||
|
|
||||||
/// The server refused the op's **request** on the opened channel
|
/// The server refused the op's **request** on the opened channel
|
||||||
/// — the SSH-REQUEST reply's `Failure` class, observed through
|
/// in the SSH-REQUEST reply's `Failure` class, observed through
|
||||||
/// the `wait()`-drain (POC-3 §D.3: Success/Failure arrive on the
|
/// the `wait()`-drain (POC-3 §D.3: Success/Failure arrive on the
|
||||||
/// channel queue, not as `Err` from the request call). The
|
/// channel queue, not as `Err` from the request call). The
|
||||||
/// channel is closed behind the refusal (the row owned it).
|
/// channel is closed behind the refusal (the row owned it).
|
||||||
@@ -315,26 +315,26 @@ pub enum OpError {
|
|||||||
RequestRefused,
|
RequestRefused,
|
||||||
|
|
||||||
/// The channel ended before the op's request reply arrived (the
|
/// The channel ended before the op's request reply arrived (the
|
||||||
/// `wait()`-drain saw the channel's end) — the op cannot
|
/// `wait()`-drain saw the channel's end); the op cannot
|
||||||
/// proceed, nothing is fabricated.
|
/// proceed, nothing is fabricated.
|
||||||
#[error("the channel ended before the op's request reply arrived")]
|
#[error("the channel ended before the op's request reply arrived")]
|
||||||
ChannelEnded,
|
ChannelEnded,
|
||||||
|
|
||||||
/// The op's command exceeds the exec-length budget
|
/// The op's command exceeds the exec-length budget
|
||||||
/// (`Limits::max_exec_command_len`) — the per-length cap's
|
/// (`Limits::max_exec_command_len`): the per-length cap's
|
||||||
/// client-mirror verdict, answered **before any wire op** (bounded
|
/// client-mirror verdict, answered **before any wire op** (bounded
|
||||||
/// resources; the server direction's pre-parse cap mirrored at
|
/// resources; the server direction's pre-parse cap mirrored at
|
||||||
/// the client's command-construction gate).
|
/// the client's command-construction gate).
|
||||||
#[error("the op's command exceeds the configured exec-command length cap")]
|
#[error("the op's command exceeds the configured exec-command length cap")]
|
||||||
CommandTooLong,
|
CommandTooLong,
|
||||||
|
|
||||||
/// The git row's repo id is outside the ADR-008 grammar — the
|
/// The git row's repo id is outside the ADR-008 grammar, the
|
||||||
/// git row's validation gate mirrored (the server direction's
|
/// git row's validation gate mirrored (the server direction's
|
||||||
/// `repo_id_validates` verdict), answered before any wire op.
|
/// `repo_id_validates` verdict), answered before any wire op.
|
||||||
#[error("the repository id is outside the repo-id grammar")]
|
#[error("the repository id is outside the repo-id grammar")]
|
||||||
InvalidRepoId,
|
InvalidRepoId,
|
||||||
|
|
||||||
/// Unix sockets are unavailable on this platform — the
|
/// Unix sockets are unavailable on this platform; this is the
|
||||||
/// `direct-streamlocal` twin's runtime-Unix gate (OQ-11:
|
/// `direct-streamlocal` twin's runtime-Unix gate (OQ-11:
|
||||||
/// universal compile, clean off-Unix decline **before any wire
|
/// universal compile, clean off-Unix decline **before any wire
|
||||||
/// op**), mirrored from the server direction's gate.
|
/// op**), mirrored from the server direction's gate.
|
||||||
@@ -344,7 +344,7 @@ pub enum OpError {
|
|||||||
/// The reverse-forward op's channel type has **no acceptor
|
/// The reverse-forward op's channel type has **no acceptor
|
||||||
/// registered** (ADR-003 §4's client-side gate surfaced to the
|
/// registered** (ADR-003 §4's client-side gate surfaced to the
|
||||||
/// *issuing* side: a reverse listen whose data leg could not serve
|
/// *issuing* side: a reverse listen whose data leg could not serve
|
||||||
/// arrivals is issued never — the half-capability posture,
|
/// arrivals is issued never; the half-capability posture,
|
||||||
/// answered before any wire op). Featured-but-unregistered is the
|
/// answered before any wire op). Featured-but-unregistered is the
|
||||||
/// exact class: the unset-declines convention mirrored (the
|
/// exact class: the unset-declines convention mirrored (the
|
||||||
/// acceptor slots are assembly-mutable, install-first is the
|
/// acceptor slots are assembly-mutable, install-first is the
|
||||||
@@ -353,27 +353,27 @@ pub enum OpError {
|
|||||||
NoAcceptor,
|
NoAcceptor,
|
||||||
|
|
||||||
/// The peer refused the op's reverse-forward global request (the
|
/// The peer refused the op's reverse-forward global request (the
|
||||||
/// `RequestDenied` wire decline class) — a listen denial or the
|
/// `RequestDenied` wire decline class); a listen denial or the
|
||||||
/// cancel forms' unknown-listener answer (sshd parity: match by
|
/// cancel forms' unknown-listener answer (sshd parity: match by
|
||||||
/// the raw wire strings). Channel-scoped honesty: the connection
|
/// the raw wire strings). Channel-scoped honesty: the connection
|
||||||
/// stays usable (row-pinned), and the wire's own reason classes
|
/// stays usable (row-pinned), and the wire's own reason classes
|
||||||
/// are never invented here — the closed family class mirrors the
|
/// are never invented here; the closed family class mirrors the
|
||||||
/// wire shape, `RequestRefused`'s global-request counterpart.
|
/// wire shape, `RequestRefused`'s global-request counterpart.
|
||||||
#[error("the peer refused the reverse-forward request")]
|
#[error("the peer refused the reverse-forward request")]
|
||||||
ListenRefused,
|
ListenRefused,
|
||||||
|
|
||||||
/// The op's reverse-listen admission hit the connection's
|
/// The op's reverse-listen admission hit the connection's
|
||||||
/// dedicated listener ceiling (the listener counter sharing
|
/// dedicated listener ceiling (the listener counter sharing
|
||||||
/// `Limits::max_concurrent_channels` — the server-side
|
/// `Limits::max_concurrent_channels`, the server-side
|
||||||
/// listener-counter posture mirrored; the cap relation recorded in
|
/// listener-counter posture mirrored; the cap relation recorded in
|
||||||
/// `client_accept.rs`) — answered **before any wire op**: a
|
/// `client_accept.rs`), answered **before any wire op**: a
|
||||||
/// request the budget cannot track is issued never.
|
/// request the budget cannot track is issued never.
|
||||||
#[error("the connection's reverse-listen count is at its ceiling")]
|
#[error("the connection's reverse-listen count is at its ceiling")]
|
||||||
ListenCapExceeded,
|
ListenCapExceeded,
|
||||||
|
|
||||||
/// The sftp route is the OQ-13 decline placeholder (the feature
|
/// The sftp route is the OQ-13 decline placeholder (the feature
|
||||||
/// exists, the route does not until alkfs lands, on either
|
/// exists, the route does not until alkfs lands, on either
|
||||||
/// direction) — answered before any wire op, never a
|
/// direction), answered before any wire op and never a
|
||||||
/// half-opened subsystem channel.
|
/// half-opened subsystem channel.
|
||||||
#[error("the sftp route is not served (the OQ-13 decline placeholder)")]
|
#[error("the sftp route is not served (the OQ-13 decline placeholder)")]
|
||||||
SftpNotServed,
|
SftpNotServed,
|
||||||
@@ -389,7 +389,7 @@ impl From<russh::Error> for OpError {
|
|||||||
/// `russh::Error::ChannelOpenFailure` and maps to
|
/// `russh::Error::ChannelOpenFailure` and maps to
|
||||||
/// [`OpError::OpenDeclined`] (family-mirrored
|
/// [`OpError::OpenDeclined`] (family-mirrored
|
||||||
/// [`OpenDeclineReason`]); a denied reverse-forward *global*
|
/// [`OpenDeclineReason`]); a denied reverse-forward *global*
|
||||||
/// request (`RequestDenied` — the listen/cancel forms' only wire
|
/// request (`RequestDenied`, the listen/cancel forms' only wire
|
||||||
/// decline shape at this pin, the rows' only producer) maps to
|
/// decline shape at this pin, the rows' only producer) maps to
|
||||||
/// [`OpError::ListenRefused`]; every other russh error stays the
|
/// [`OpError::ListenRefused`]; every other russh error stays the
|
||||||
/// transparent [`OpError::Wire`].
|
/// transparent [`OpError::Wire`].
|
||||||
@@ -510,7 +510,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The host-key policy's `false` verdict is russh's `UnknownKey`
|
/// The host-key policy's `false` verdict is russh's `UnknownKey`
|
||||||
/// wire error — the client's `check_server_key` hook is the only
|
/// wire error; the client's `check_server_key` hook is the only
|
||||||
/// producer, so the transparent mapping upgrades exactly that one
|
/// producer, so the transparent mapping upgrades exactly that one
|
||||||
/// class and leaves everything else `Wire`.
|
/// class and leaves everything else `Wire`.
|
||||||
///
|
///
|
||||||
@@ -602,7 +602,7 @@ mod tests {
|
|||||||
|
|
||||||
/// - *Contract stamp:* `client.md` §op rows (no vanilla-concept
|
/// - *Contract stamp:* `client.md` §op rows (no vanilla-concept
|
||||||
/// leaks up) + POC-1 §6 (the typed open's decline resolves on
|
/// leaks up) + POC-1 §6 (the typed open's decline resolves on
|
||||||
/// the awaiting call) — the wire class mirrors one-to-one into
|
/// the awaiting call); the wire class mirrors one-to-one into
|
||||||
/// the family vocabulary, `Other` verbatim, nothing collapsed.
|
/// the family vocabulary, `Other` verbatim, nothing collapsed.
|
||||||
#[test]
|
#[test]
|
||||||
fn open_decline_mirror_preserves_every_class() {
|
fn open_decline_mirror_preserves_every_class() {
|
||||||
@@ -653,7 +653,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the reverse-forward global request's wire
|
/// - *Contract stamp:* the reverse-forward global request's wire
|
||||||
/// decline class (`RequestDenied` — the listen/cancel forms'
|
/// decline class (`RequestDenied`, the listen/cancel forms'
|
||||||
/// only producer shape at this pin): `ListenRefused`, never the
|
/// only producer shape at this pin): `ListenRefused`, never the
|
||||||
/// transparent wire class; the typed-open mapping unchanged.
|
/// transparent wire class; the typed-open mapping unchanged.
|
||||||
#[test]
|
#[test]
|
||||||
@@ -662,7 +662,7 @@ mod tests {
|
|||||||
assert!(matches!(mapped, OpError::ListenRefused), "got {mapped:?}");
|
assert!(matches!(mapped, OpError::ListenRefused), "got {mapped:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* POC-1 §6 — a rejected channel open
|
/// - *Contract stamp:* POC-1 §6, in which a rejected channel open
|
||||||
/// surfaces on the awaiting typed call as
|
/// surfaces on the awaiting typed call as
|
||||||
/// `russh::Error::ChannelOpenFailure(..)`; the op rows map it
|
/// `russh::Error::ChannelOpenFailure(..)`; the op rows map it
|
||||||
/// into [`OpError::OpenDeclined`] and every other wire error
|
/// into [`OpError::OpenDeclined`] and every other wire error
|
||||||
|
|||||||
+29
-29
@@ -1,4 +1,4 @@
|
|||||||
//! The exit-currency module — mapping.md §Exit chunk's derivation
|
//! The exit-currency module: mapping.md §Exit chunk's derivation
|
||||||
//! contract (rules 1–6, normative) as code, *shared by both
|
//! contract (rules 1–6, normative) as code, *shared by both
|
||||||
//! directions* (`w4-op-rows` extracted it out of `tty_map.rs`, whose
|
//! directions* (`w4-op-rows` extracted it out of `tty_map.rs`, whose
|
||||||
//! server-side session rows were its first and only consumer).
|
//! server-side session rows were its first and only consumer).
|
||||||
@@ -12,27 +12,27 @@
|
|||||||
//! exactly one SSH exit report shape, **never widened**.
|
//! exactly one SSH exit report shape, **never widened**.
|
||||||
//! - **The client direction** (`w4-op-rows`): the SSH exit report →
|
//! - **The client direction** (`w4-op-rows`): the SSH exit report →
|
||||||
//! the family `i32` code ([`signal_code_from_name`] and the
|
//! the family `i32` code ([`signal_code_from_name`] and the
|
||||||
//! direct `u32 → i32` identity) — the exact inverse arm, riding the
|
//! direct `u32 → i32` identity), the exact inverse arm riding the
|
||||||
//! same table. The mirror consumes the convention as-is, including
|
//! same table. The mirror consumes the convention as-is, including
|
||||||
//! its one collision (the `−1` sentinel vs a genuine SIGHUP —
|
//! its one collision (the `−1` sentinel vs a genuine SIGHUP;
|
||||||
//! mapping.md rule 3's symmetric-fidelity cost).
|
//! mapping.md rule 3's symmetric-fidelity cost).
|
||||||
//!
|
//!
|
||||||
//! No widening in either arm: the chunk is not enriched, the wire
|
//! No widening in either arm: the chunk is not enriched, the wire
|
||||||
//! report is not enriched, and the *core_dumped* bool is never
|
//! report is not enriched, and the *core_dumped* bool is never
|
||||||
//! asserted (alktty discards it — nothing better is knowable).
|
//! asserted (alktty discards it; nothing better is knowable).
|
||||||
//!
|
//!
|
||||||
//! ## The server arm's rules (mapping.md, normative)
|
//! ## The server arm's rules (mapping.md, normative)
|
||||||
//!
|
//!
|
||||||
//! 1. `code ≥ 0` → `exit-status(N)`.
|
//! 1. `code ≥ 0` → `exit-status(N)`.
|
||||||
//! 2. negative `−N`, N ≥ 2 → `exit-signal(name)` via the crate-local
|
//! 2. negative `−N`, N ≥ 2 → `exit-signal(name)` via the crate-local
|
||||||
//! **Unix signum→name table** ([`SIGNAL_NAMES`]). Server-side
|
//! **Unix signum→name table** ([`SIGNAL_NAMES`]). Server-side
|
||||||
//! work — the signal semantics belong to the serving platform
|
//! work: the signal semantics belong to the serving platform
|
||||||
//! where the backend reaps. Deliberately *not* derived from
|
//! where the backend reaps. Deliberately *not* derived from
|
||||||
//! alktty's `signal_from_name` inverse, which is a forward
|
//! alktty's `signal_from_name` inverse, which is a forward
|
||||||
//! name→number map for the control row, not a derivation table.
|
//! name→number map for the control row, not a derivation table.
|
||||||
//! `core_dumped` is **never asserted** — SSH's `exit-signal` bool
|
//! `core_dumped` is **never asserted**; SSH's `exit-signal` bool
|
||||||
//! stays false on every row.
|
//! stays false on every row.
|
||||||
//! 3. `−1` → `exit-status(255)`, never a signal claim — the
|
//! 3. `−1` → `exit-status(255)`, never a signal claim: the
|
||||||
//! could-not-determine sentinel collides with the SIGHUP encoding
|
//! could-not-determine sentinel collides with the SIGHUP encoding
|
||||||
//! on the pipe path, accepted symmetrically per the mapping:
|
//! on the pipe path, accepted symmetrically per the mapping:
|
||||||
//! neither a fabricated signal for a wait failure nor a real one.
|
//! neither a fabricated signal for a wait failure nor a real one.
|
||||||
@@ -41,13 +41,13 @@
|
|||||||
//! 5. **The PTY-path fidelity limit (upstream)**: portable-pty 0.9
|
//! 5. **The PTY-path fidelity limit (upstream)**: portable-pty 0.9
|
||||||
//! reaps a signal-terminated Unix child as a plain code (the
|
//! reaps a signal-terminated Unix child as a plain code (the
|
||||||
//! signal number is unrecoverable through its trait surface), so
|
//! signal number is unrecoverable through its trait surface), so
|
||||||
//! signal-terminated PTY sessions arrive as `exit-status(1)` — a
|
//! signal-terminated PTY sessions arrive as `exit-status(1)`, a
|
||||||
//! documented alktty/portable-pty ask; this table in practice
|
//! documented alktty/portable-pty ask; this table in practice
|
||||||
//! sees the *pipe* path's negative encoding only. Faithful
|
//! sees the *pipe* path's negative encoding only. Faithful
|
||||||
//! `exit-signal` there needs the alktty backend to conform to its
|
//! `exit-signal` there needs the alktty backend to conform to its
|
||||||
//! own ADR's negative-code convention; alkssh's contract adapts
|
//! own ADR's negative-code convention; alkssh's contract adapts
|
||||||
//! automatically.
|
//! automatically.
|
||||||
//! 6. Unmapped negatives → rule 3 (`exit-status(255)`) — the mapping
|
//! 6. Unmapped negatives → rule 3 (`exit-status(255)`); the mapping
|
||||||
//! never invents a name (defensive case: the convention's
|
//! never invents a name (defensive case: the convention's
|
||||||
//! producers are `−1` and `−signum` only).
|
//! producers are `−1` and `−signum` only).
|
||||||
//!
|
//!
|
||||||
@@ -56,26 +56,26 @@
|
|||||||
//! 1. `exit-status(N)` → the family code `N` (direct identity).
|
//! 1. `exit-status(N)` → the family code `N` (direct identity).
|
||||||
//! 2. `exit-signal(name)` → the family code `−N` via the same
|
//! 2. `exit-signal(name)` → the family code `−N` via the same
|
||||||
//! [`SIGNAL_NAMES`] table read backwards
|
//! [`SIGNAL_NAMES`] table read backwards
|
||||||
//! ([`signal_code_from_name`]) — the mapping never invents a
|
//! ([`signal_code_from_name`]); the mapping never invents a
|
||||||
//! signum.
|
//! signum.
|
||||||
//! 3. A name with no table entry → `−1` (the sentinel; the same
|
//! 3. A name with no table entry → `−1` (the sentinel; the same
|
||||||
//! honest could-not-determine semantic rule 3 consumes on the
|
//! honest could-not-determine semantic rule 3 consumes on the
|
||||||
//! server arm — including a genuine SIGHUP, whose mirror rides
|
//! server arm, including a genuine SIGHUP, whose mirror rides
|
||||||
//! `−1` by the same accepted collision).
|
//! `−1` by the same accepted collision).
|
||||||
//! 4. No exit report at all ends a session with none — the
|
//! 4. No exit report at all ends a session with none, the
|
||||||
//! no-exit-chunk close family's mirror ("reports nothing",
|
//! no-exit-chunk close family's mirror ("reports nothing",
|
||||||
//! mapping.md): the caller observes the session's end, never a
|
//! mapping.md): the caller observes the session's end, never a
|
||||||
//! fabricated code.
|
//! fabricated code.
|
||||||
|
|
||||||
/// One SSH exit report shape derived from an exit chunk —
|
/// One SSH exit report shape derived from an exit chunk;
|
||||||
/// [`derive_exit`]'s outcome.
|
/// [`derive_exit`]'s outcome.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
pub(crate) enum ExitReport {
|
pub(crate) enum ExitReport {
|
||||||
/// `exit-status(N)` — codes ≥ 0, the `−1` sentinel, and every
|
/// `exit-status(N)`: codes ≥ 0, the `−1` sentinel, and every
|
||||||
/// unmapped negative (rules 1, 3, 4, 6).
|
/// unmapped negative (rules 1, 3, 4, 6).
|
||||||
Status(u32),
|
Status(u32),
|
||||||
/// `exit-signal(name)` — a mapped negative `−N`, N ≥ 2 (rule 2).
|
/// `exit-signal(name)`: a mapped negative `−N` where N ≥ 2 (rule 2).
|
||||||
/// The name is the table's value, never an invention.
|
/// The name is the table's value, never an invention.
|
||||||
Signal(&'static str),
|
Signal(&'static str),
|
||||||
}
|
}
|
||||||
@@ -83,11 +83,11 @@ pub(crate) enum ExitReport {
|
|||||||
/// The crate-local Unix signum→name table (server arm rule 2 / client
|
/// The crate-local Unix signum→name table (server arm rule 2 / client
|
||||||
/// arm rule 2): the 1–31 conventional Unix signals, as the exit
|
/// arm rule 2): the 1–31 conventional Unix signals, as the exit
|
||||||
/// chunk's producers encode them (`−signum`) and as the wire's
|
/// chunk's producers encode them (`−signum`) and as the wire's
|
||||||
/// `exit-signal` senders name them. Server-side work — the signal
|
/// `exit-signal` senders name them. Server-side work: the signal
|
||||||
/// semantics belong to the serving platform where the backend reaps.
|
/// semantics belong to the serving platform where the backend reaps.
|
||||||
/// Deliberately **not** derived from alktty's `signal_from_name` (a
|
/// Deliberately **not** derived from alktty's `signal_from_name` (a
|
||||||
/// forward name→number map for the control row, not a derivation
|
/// forward name→number map for the control row, not a derivation
|
||||||
/// table; deliberately narrower too — it exists for what a front-end
|
/// table; deliberately narrower too, existing for what a front-end
|
||||||
/// sends, not for what a child can die by).
|
/// sends, not for what a child can die by).
|
||||||
pub(crate) const SIGNAL_NAMES: &[(u32, &str)] = &[
|
pub(crate) const SIGNAL_NAMES: &[(u32, &str)] = &[
|
||||||
(1, "HUP"),
|
(1, "HUP"),
|
||||||
@@ -124,7 +124,7 @@ pub(crate) const SIGNAL_NAMES: &[(u32, &str)] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
/// The derivation contract's whole decision procedure (the §module
|
/// The derivation contract's whole decision procedure (the §module
|
||||||
/// docs' rules 1–6 — normative).
|
/// docs' rules 1–6; normative).
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
pub(crate) fn derive_exit(code: i32) -> ExitReport {
|
pub(crate) fn derive_exit(code: i32) -> ExitReport {
|
||||||
match code {
|
match code {
|
||||||
@@ -143,7 +143,7 @@ pub(crate) fn derive_exit(code: i32) -> ExitReport {
|
|||||||
/// The client arm's inverse read (§module docs, rules 1–3): the SSH
|
/// The client arm's inverse read (§module docs, rules 1–3): the SSH
|
||||||
/// `exit-signal` sender's name → the family exit-code convention's
|
/// `exit-signal` sender's name → the family exit-code convention's
|
||||||
/// value (`−signum`), `−1` for a name with no table entry (the
|
/// value (`−signum`), `−1` for a name with no table entry (the
|
||||||
/// sentinel — the mapping never invents a signum; a genuine SIGHUP
|
/// sentinel; the mapping never invents a signum, and a genuine SIGHUP
|
||||||
/// rides it by the same accepted collision).
|
/// rides it by the same accepted collision).
|
||||||
pub(crate) fn signal_code_from_name(name: &str) -> i32 {
|
pub(crate) fn signal_code_from_name(name: &str) -> i32 {
|
||||||
match SIGNAL_NAMES.iter().find(|(_, n)| *n == name) {
|
match SIGNAL_NAMES.iter().find(|(_, n)| *n == name) {
|
||||||
@@ -155,7 +155,7 @@ pub(crate) fn signal_code_from_name(name: &str) -> i32 {
|
|||||||
/// The table's name → russh's `Sig` vocabulary (the
|
/// The table's name → russh's `Sig` vocabulary (the
|
||||||
/// `exit_signal_request` parameter). russh's known set rides its
|
/// `exit_signal_request` parameter). russh's known set rides its
|
||||||
/// variant names; anything else rides `Sig::Custom` (RFC 4254 §6.10's
|
/// variant names; anything else rides `Sig::Custom` (RFC 4254 §6.10's
|
||||||
/// signal-name encoding — vanilla clients parse arbitrary names).
|
/// signal-name encoding, since vanilla clients parse arbitrary names).
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
pub(crate) fn sig_from_name(name: &str) -> russh::Sig {
|
pub(crate) fn sig_from_name(name: &str) -> russh::Sig {
|
||||||
match name {
|
match name {
|
||||||
@@ -177,7 +177,7 @@ pub(crate) fn sig_from_name(name: &str) -> russh::Sig {
|
|||||||
|
|
||||||
/// The SSH signal row's name translation: russh's `Sig` → the name
|
/// The SSH signal row's name translation: russh's `Sig` → the name
|
||||||
/// string alktty's control vocabulary carries (the uppercase names;
|
/// string alktty's control vocabulary carries (the uppercase names;
|
||||||
/// `Custom` carrying its own). Names flow to the backend verbatim —
|
/// `Custom` carrying its own). Names flow to the backend verbatim,
|
||||||
/// no alkssh-side filter (mapping.md §the control rows; the client
|
/// no alkssh-side filter (mapping.md §the control rows; the client
|
||||||
/// arm's `exit-signal` consumption reads through this before
|
/// arm's `exit-signal` consumption reads through this before
|
||||||
/// [`signal_code_from_name`]).
|
/// [`signal_code_from_name`]).
|
||||||
@@ -207,7 +207,7 @@ mod tests {
|
|||||||
use russh::Sig;
|
use russh::Sig;
|
||||||
|
|
||||||
/// - *Contract stamp:* mapping.md §Exit chunk rule 1 (`code ≥ 0`
|
/// - *Contract stamp:* mapping.md §Exit chunk rule 1 (`code ≥ 0`
|
||||||
/// → `exit-status(N)` — the chunk carries the whole truth for
|
/// → `exit-status(N)`; the chunk carries the whole truth for
|
||||||
/// clean exits).
|
/// clean exits).
|
||||||
#[test]
|
#[test]
|
||||||
fn non_negative_codes_map_to_exit_status() {
|
fn non_negative_codes_map_to_exit_status() {
|
||||||
@@ -218,8 +218,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* mapping.md §Exit chunk rule 2 (negative
|
/// - *Contract stamp:* mapping.md §Exit chunk rule 2 (negative
|
||||||
/// `−N`, N ≥ 2 → `exit-signal(name)` via the table — the POC-3
|
/// `−N`, N ≥ 2 → `exit-signal(name)` via the table; the POC-3
|
||||||
/// §D.2 live evidence: `−15` → `exit-signal("TERM")`).
|
/// §D.2 live evidence is `−15` → `exit-signal("TERM")`).
|
||||||
#[test]
|
#[test]
|
||||||
fn negative_signals_map_through_the_table() {
|
fn negative_signals_map_through_the_table() {
|
||||||
for (signum, name) in SIGNAL_NAMES.iter().skip(1) {
|
for (signum, name) in SIGNAL_NAMES.iter().skip(1) {
|
||||||
@@ -230,7 +230,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* mapping.md §Exit chunk rule 3 (`−1` →
|
/// - *Contract stamp:* mapping.md §Exit chunk rule 3 (`−1` →
|
||||||
/// `exit-status(255)`, never a signal claim — the sentinel/
|
/// `exit-status(255)`, never a signal claim; the sentinel/
|
||||||
/// SIGHUP collision accepted symmetrically).
|
/// SIGHUP collision accepted symmetrically).
|
||||||
#[test]
|
#[test]
|
||||||
fn the_sentinel_is_not_a_signal() {
|
fn the_sentinel_is_not_a_signal() {
|
||||||
@@ -238,7 +238,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* mapping.md §Exit chunk rule 6 (unmapped
|
/// - *Contract stamp:* mapping.md §Exit chunk rule 6 (unmapped
|
||||||
/// negatives → rule 3 — the mapping never invents a name).
|
/// negatives → rule 3; the mapping never invents a name).
|
||||||
#[test]
|
#[test]
|
||||||
fn unmapped_negatives_report_255() {
|
fn unmapped_negatives_report_255() {
|
||||||
for code in [i32::MIN, -33, -32, -100, -1_000_000] {
|
for code in [i32::MIN, -33, -32, -100, -1_000_000] {
|
||||||
@@ -246,7 +246,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the table bound — every entry is the
|
/// - *Contract stamp:* the table bound: every entry is the
|
||||||
/// conventional Unix signal set 1–31, sorted, names unique and
|
/// conventional Unix signal set 1–31, sorted, names unique and
|
||||||
/// uppercase (the vocabulary the derivation table promises).
|
/// uppercase (the vocabulary the derivation table promises).
|
||||||
#[test]
|
#[test]
|
||||||
@@ -282,7 +282,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The client arm's mirror row — runnable in every build the arm
|
/// The client arm's mirror row, runnable in every build the arm
|
||||||
/// compiles in (session or git; the git-only builds carry the client
|
/// compiles in (session or git; the git-only builds carry the client
|
||||||
/// rows too).
|
/// rows too).
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -290,7 +290,7 @@ mod tests {
|
|||||||
mod client_arm_rows {
|
mod client_arm_rows {
|
||||||
use super::SIGNAL_NAMES;
|
use super::SIGNAL_NAMES;
|
||||||
|
|
||||||
/// - *Contract stamp:* the client arm (§module docs' rules 1–3) —
|
/// - *Contract stamp:* the client arm (§module docs' rules 1–3),
|
||||||
/// the inverse read through the same table, never widened:
|
/// the inverse read through the same table, never widened:
|
||||||
/// mapped names ride `−signum`, unmapped names ride the `−1`
|
/// mapped names ride `−signum`, unmapped names ride the `−1`
|
||||||
/// sentinel (the mapping never invents a signum).
|
/// sentinel (the mapping never invents a signum).
|
||||||
|
|||||||
+3
-3
@@ -6,7 +6,7 @@
|
|||||||
//! path (client) surfaces it at `check_server_key`, and assembly
|
//! path (client) surfaces it at `check_server_key`, and assembly
|
||||||
//! registrations store it as a string. The string format
|
//! registrations store it as a string. The string format
|
||||||
//! (`SSHFP`-style `SHA256:` prefix over the OpenSSH wire-key digest,
|
//! (`SSHFP`-style `SHA256:` prefix over the OpenSSH wire-key digest,
|
||||||
//! unpadded base64) is a **contract invariant** — registration strings
|
//! unpadded base64) is a **contract invariant**: registration strings
|
||||||
//! from any standard issuer (`ssh-keygen -lf`, alkcall-side
|
//! from any standard issuer (`ssh-keygen -lf`, alkcall-side
|
||||||
//! computation) must compare equal to what this crate computes.
|
//! computation) must compare equal to what this crate computes.
|
||||||
//!
|
//!
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
|
|
||||||
use russh::keys::{HashAlg, PublicKey};
|
use russh::keys::{HashAlg, PublicKey};
|
||||||
|
|
||||||
/// Computes a public key's fingerprint — the single fingerprint path
|
/// Computes a public key's fingerprint, the single fingerprint path
|
||||||
/// in the crate (ADR-004 decision 1's contract invariant).
|
/// in the crate (ADR-004 decision 1's contract invariant).
|
||||||
///
|
///
|
||||||
/// The returned string is the OpenSSH fingerprint format: `SHA256:`
|
/// The returned string is the OpenSSH fingerprint format: `SHA256:`
|
||||||
/// followed by the unpadded base64 of the SHA-256 digest over the
|
/// followed by the unpadded base64 of the SHA-256 digest over the
|
||||||
/// key's OpenSSH wire encoding, byte-identical to independent issuers
|
/// key's OpenSSH wire encoding, byte-identical to independent issuers
|
||||||
/// (`ssh-keygen -lf`) — parity is proven, not assumed. Every
|
/// (`ssh-keygen -lf`); parity is proven, not assumed. Every
|
||||||
/// fingerprint comparison in the crate and every registration value an
|
/// fingerprint comparison in the crate and every registration value an
|
||||||
/// assembly stores is this string; no other formatting path exists.
|
/// assembly stores is this string; no other formatting path exists.
|
||||||
pub fn fingerprint(public_key: &PublicKey) -> String {
|
pub fn fingerprint(public_key: &PublicKey) -> String {
|
||||||
|
|||||||
+30
-30
@@ -1,4 +1,4 @@
|
|||||||
//! The git mapping (`w3-git-mapping`) — the git branch of the spine
|
//! The git mapping (`w3-git-mapping`), the git branch of the spine
|
||||||
//! (ADR-006 decision 2, exec-only, the alkforge primary purpose): the
|
//! (ADR-006 decision 2, exec-only, the alkforge primary purpose): the
|
||||||
//! dispatch's [`ExecRoute::Git`](crate::dispatch::ExecRoute) route
|
//! dispatch's [`ExecRoute::Git`](crate::dispatch::ExecRoute) route
|
||||||
//! dispatches to the published alkgit seam,
|
//! dispatches to the published alkgit seam,
|
||||||
@@ -10,33 +10,33 @@
|
|||||||
//!
|
//!
|
||||||
//! ## The decisions recorded here (the task's decide-and-record set)
|
//! ## The decisions recorded here (the task's decide-and-record set)
|
||||||
//!
|
//!
|
||||||
//! - **Repo id — validate, never resolve.** The parse stage
|
//! - **Repo id: validate, never resolve.** The parse stage
|
||||||
//! ([`crate::dispatch`]) normalizes the wire path; this stage
|
//! ([`crate::dispatch`]) normalizes the wire path; this stage
|
||||||
//! validates the id against `alkgit::validate_repo_id` (1–2
|
//! validates the id against `alkgit::validate_repo_id` (1–2
|
||||||
//! segments, charset `a-z0-9-_`, ≤ 100 bytes — the ADR-008
|
//! segments, charset `a-z0-9-_`, ≤ 100 bytes, the ADR-008
|
||||||
//! repo-id grammar) and hands it through untouched to
|
//! repo-id grammar) and hands it through untouched to
|
||||||
//! `serve_duplex_resolved`. **alkssh never resolves**: the
|
//! `serve_duplex_resolved`. **alkssh never resolves**: the
|
||||||
//! resolution, the authorize policy and the collapsed generic
|
//! resolution, the authorize policy and the collapsed generic
|
||||||
//! denial (unknown ≡ unauthorized, one wire shape, no existence
|
//! denial (unknown ≡ unauthorized, one wire shape, no existence
|
||||||
//! oracle) live at the seam. An id failing the *shape* gate is
|
//! oracle) live at the seam. An id failing the *shape* gate is
|
||||||
//! declined here — `invalid-repo-id`, request-level, reason-only on
|
//! declined here as `invalid-repo-id`, request-level, reason-only on
|
||||||
//! the wire (the reply-vocabulary decision in [`crate::dispatch`]).
|
//! the wire (the reply-vocabulary decision in [`crate::dispatch`]).
|
||||||
//!
|
//!
|
||||||
//! - **Wire shapes per decline class** (the open/reply vocabulary):
|
//! - **Wire shapes per decline class** (the open/reply vocabulary):
|
||||||
//! - *Request-level* declinables (`unknown-service`, `service-only`
|
//! - *Request-level* declinables (`unknown-service` and `service-only`
|
||||||
//! — the parse core's; `invalid-repo-id` — this module's gate):
|
//! from the parse core; `invalid-repo-id` from this module's gate):
|
||||||
//! `channel_failure`, channel left usable. The reply vocabulary is
|
//! `channel_failure`, channel left usable. The reply vocabulary is
|
||||||
//! shared with parse-then-fail; the reason lives in the events
|
//! shared with parse-then-fail; the reason lives in the events
|
||||||
//! only.
|
//! only.
|
||||||
//! - *Serving-path* declines (the row owned the channel):
|
//! - *Serving-path* declines (the row owned the channel):
|
||||||
//! seam-unconfigured (feature on, [`crate::GitSeam`] absent) and
|
//! seam-unconfigured (feature on, [`crate::GitSeam`] absent) and
|
||||||
//! the identity-less guard — failure + close, from the spawned
|
//! the identity-less guard, failure + close from the spawned
|
||||||
//! task, the wave-2 seam-stub shape.
|
//! task, the wave-2 seam-stub shape.
|
||||||
//! - *The seam's own denials* are not alkssh declines: an unknown or
|
//! - *The seam's own denials* are not alkssh declines: an unknown or
|
||||||
//! ungranted repo flows to `serve_duplex_resolved`, whose
|
//! ungranted repo flows to `serve_duplex_resolved`, whose
|
||||||
//! collapsed generic denial writes the `ERR access denied: <repo>`
|
//! collapsed generic denial writes the `ERR access denied: <repo>`
|
||||||
//! band over the channel **before any pack/ref byte**
|
//! band over the channel **before any pack/ref byte**
|
||||||
//! (`SubstrateError::AccessDenied`) — the POC-3 §A.5 byte-order
|
//! (`SubstrateError::AccessDenied`); the POC-3 §A.5 byte-order
|
||||||
//! evidence, contract-pinned by the denial rows in
|
//! evidence, contract-pinned by the denial rows in
|
||||||
//! `tests/git_mapping.rs`.
|
//! `tests/git_mapping.rs`.
|
||||||
//!
|
//!
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
//! built from the channel's **split halves**, never
|
//! built from the channel's **split halves**, never
|
||||||
//! `Channel::into_stream()`. `into_stream` wraps the whole channel
|
//! `Channel::into_stream()`. `into_stream` wraps the whole channel
|
||||||
//! in russh's `ChannelCloseOnDrop`, whose `Drop` sends
|
//! in russh's `ChannelCloseOnDrop`, whose `Drop` sends
|
||||||
//! server→client `CHANNEL_CLOSE` from a *spawned task* — inside the
|
//! server→client `CHANNEL_CLOSE` from a *spawned task* inside the
|
||||||
//! seam call, racing [`serve_git_row`]'s explicit
|
//! seam call, racing [`serve_git_row`]'s explicit
|
||||||
//! `exit_status_request` for a queue position on the same bounded
|
//! `exit_status_request` for a queue position on the same bounded
|
||||||
//! `Sender<Msg>` (russh's `event_buffer_size`, default 10): a full
|
//! `Sender<Msg>` (russh's `event_buffer_size`, default 10): a full
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
//! let the loop remove the channel, and silently no-op the report.
|
//! let the loop remove the channel, and silently no-op the report.
|
||||||
//! The split-halves shape removes the competitor instead of racing
|
//! The split-halves shape removes the competitor instead of racing
|
||||||
//! it: russh 0.65 keeps its `ChannelRx`/`ChannelTx` private, so
|
//! it: russh 0.65 keeps its `ChannelRx`/`ChannelTx` private, so
|
||||||
//! [`GitChannelDuplex`] is built from the public API — the reader
|
//! [`GitChannelDuplex`] is built from the public API: the reader
|
||||||
//! is the module's [`ChannelHalveReader`] (an owned read half
|
//! is the module's [`ChannelHalveReader`] (an owned read half
|
||||||
//! repinning the private `ChannelRx`'s observation semantics
|
//! repinning the private `ChannelRx`'s observation semantics
|
||||||
//! without the close-on-drop wrapper; its only `Drop` behavior
|
//! without the close-on-drop wrapper; its only `Drop` behavior
|
||||||
@@ -62,14 +62,14 @@
|
|||||||
//! sends EOF, exactly `into_stream`'s). Nothing the seam drops ever
|
//! sends EOF, exactly `into_stream`'s). Nothing the seam drops ever
|
||||||
//! messages the wire, so the report's send is the sole
|
//! messages the wire, so the report's send is the sole
|
||||||
//! close-contracting sender, and its queue position ahead of the
|
//! close-contracting sender, and its queue position ahead of the
|
||||||
//! explicit close holds by construction — sequential awaited sends
|
//! explicit close holds by construction: sequential awaited sends
|
||||||
//! on the one `Handle`, saturated buffer or not. The tty rows are
|
//! on the one `Handle`, saturated buffer or not. The tty rows are
|
||||||
//! not in this class: their report precedes any close-contracting
|
//! not in this class: their report precedes any close-contracting
|
||||||
//! drop within one task — the wave-3 gate verified.
|
//! drop within one task (the wave-3 gate verified).
|
||||||
//!
|
//!
|
||||||
//! - **Exit-status mapping** (POC-3 §A.5, decided):
|
//! - **Exit-status mapping** (POC-3 §A.5, decided):
|
||||||
//! [`serve_git_row`] maps the serve outcome onto exactly two
|
//! [`serve_git_row`] maps the serve outcome onto exactly two
|
||||||
//! codes — `Ok(())` → `exit-status(0)` (vanilla git exits 0 on
|
//! codes: `Ok(())` → `exit-status(0)` (vanilla git exits 0 on
|
||||||
//! success) and every `SubstrateError` → `exit-status(1)`. `1` for
|
//! success) and every `SubstrateError` → `exit-status(1)`. `1` for
|
||||||
//! `AccessDenied` is the POC's wire evidence (the russh client
|
//! `AccessDenied` is the POC's wire evidence (the russh client
|
||||||
//! observed `ExitStatus{1}` after the denial band); `Io`/`Protocol`
|
//! observed `ExitStatus{1}` after the denial band); `Io`/`Protocol`
|
||||||
@@ -77,32 +77,32 @@
|
|||||||
//! machinery-misuse variants (`ResetRequired`/`WriterTaken`/
|
//! machinery-misuse variants (`ResetRequired`/`WriterTaken`/
|
||||||
//! `RawTailUnavailable`) never arise from the serving entry
|
//! `RawTailUnavailable`) never arise from the serving entry
|
||||||
//! (substrate-internal misuse, never a wire shape) and are
|
//! (substrate-internal misuse, never a wire shape) and are
|
||||||
//! defensively mapped to `1`. `255` is never fabricated — it is
|
//! defensively mapped to `1`. `255` is never fabricated; it is
|
||||||
//! OpenSSH's could-not-start semantic, which this door never claims.
|
//! OpenSSH's could-not-start semantic, which this door never claims.
|
||||||
//! The status is sent **before** close, from the spawned serving
|
//! The status is sent **before** close, from the spawned serving
|
||||||
//! task via the cloned `Session::handle()`
|
//! task via the cloned `Session::handle()`
|
||||||
//! ([`crate::serve`]'s mechanics; `mapping.md`'s exit_status row).
|
//! ([`crate::serve`]'s mechanics; `mapping.md`'s exit_status row).
|
||||||
//!
|
//!
|
||||||
//! - **Identity ride** (`w2-auth-bridge`'s parked item, discharged
|
//! - **Identity ride** (`w2-auth-bridge`'s parked item, discharged
|
||||||
//! here): the resolved identity — the bridge's
|
//! here): the resolved identity (the bridge's
|
||||||
//! `auth_succeeded`-state ride-along in handler state — is cloned
|
//! `auth_succeeded`-state ride-along in handler state) is cloned
|
||||||
//! into the serving task and passed on
|
//! into the serving task and passed on
|
||||||
//! `serve_duplex_resolved`'s identity param, so the visible-surface
|
//! `serve_duplex_resolved`'s identity param, so the visible-surface
|
||||||
//! invariant is observable through the git row: alkgit's
|
//! invariant is observable through the git row: alkgit's
|
||||||
//! `authorize` evaluation is the live witness (a granted-private
|
//! `authorize` evaluation is the live witness (a granted-private
|
||||||
//! repo serves, an ungranted one denies — the harness rows pin
|
//! repo serves, an ungranted one denies; the harness rows pin
|
||||||
//! both). A handler state without an identity is unreachable (russh
|
//! both). A handler state without an identity is unreachable (russh
|
||||||
//! authorizes before channels exist) and declines serving-path
|
//! authorizes before channels exist) and declines serving-path
|
||||||
//! (`no-identity`) — never anonymous serving.
|
//! (`no-identity`), never anonymous serving.
|
||||||
//!
|
//!
|
||||||
//! - **Process-mode dialect**: alkgit speaks first (fetch: the V2
|
//! - **Process-mode dialect**: alkgit speaks first (fetch: the V2
|
||||||
//! advertisement, no `# service=` prefix; push: the V0 ref
|
//! advertisement, no `# service=` prefix; push: the V0 ref
|
||||||
//! advertisement) — nothing alkssh-side; the byte-level rows in
|
//! advertisement), nothing alkssh-side; the byte-level rows in
|
||||||
//! `tests/git_mapping.rs` verify the dialect holds over the channel
|
//! `tests/git_mapping.rs` verify the dialect holds over the channel
|
||||||
//! duplex.
|
//! duplex.
|
||||||
//!
|
//!
|
||||||
//! The serve outcome's event shape: the serving-path lifecycle only
|
//! The serve outcome's event shape: the serving-path lifecycle only
|
||||||
//! (started / ended — the vocabulary's `warn`-failed when the wire
|
//! (started / ended, with the vocabulary's `warn`-failed when the wire
|
||||||
//! reporting breaks). A serve `Err` is *not* a decline event: the
|
//! reporting breaks). A serve `Err` is *not* a decline event: the
|
||||||
//! substrate already mapped its own error onto the wire (bands, the
|
//! substrate already mapped its own error onto the wire (bands, the
|
||||||
//! denial), and the client's observation is the exit-status above.
|
//! denial), and the client's observation is the exit-status above.
|
||||||
@@ -123,7 +123,7 @@ use crate::dispatch::GitServiceRoute;
|
|||||||
use crate::observe;
|
use crate::observe;
|
||||||
|
|
||||||
/// The git row's validation gate: the id's shape against
|
/// The git row's validation gate: the id's shape against
|
||||||
/// `alkgit::validate_repo_id` — the ADR-008 repo-id grammar for
|
/// `alkgit::validate_repo_id`, the ADR-008 repo-id grammar for
|
||||||
/// *rejection* only (the mapping-free posture; the id is never
|
/// *rejection* only (the mapping-free posture; the id is never
|
||||||
/// decomposed, joined, or normalized into paths beyond
|
/// decomposed, joined, or normalized into paths beyond
|
||||||
/// [`crate::dispatch`]'s quote/slash/`.git` stage).
|
/// [`crate::dispatch`]'s quote/slash/`.git` stage).
|
||||||
@@ -150,13 +150,13 @@ fn exit_code(outcome: &Result<(), SubstrateError>) -> u32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The seam's duplex over the **split** channel halves — the
|
/// The seam's duplex over the **split** channel halves, the
|
||||||
/// status-before-close shape's carrier (the module doc's
|
/// status-before-close shape's carrier (the module doc's
|
||||||
/// §Status-before-close decision). russh 0.65 keeps its `ChannelRx`/
|
/// §Status-before-close decision). russh 0.65 keeps its `ChannelRx`/
|
||||||
/// `ChannelTx` types private, so both halves are built from public
|
/// `ChannelTx` types private, so both halves are built from public
|
||||||
/// API: the reader is a [`ChannelHalveReader`] (an owned close-free
|
/// API: the reader is a [`ChannelHalveReader`] (an owned close-free
|
||||||
/// read half), the writer the write half's `ChannelTx` via
|
/// read half), the writer the write half's `ChannelTx` via
|
||||||
/// `make_writer_ext(None)` — its `poll_shutdown` sends EOF, exactly
|
/// `make_writer_ext(None)`; its `poll_shutdown` sends EOF, exactly
|
||||||
/// `into_stream`'s semantics, its `Drop` messages nothing (the
|
/// `into_stream`'s semantics, its `Drop` messages nothing (the
|
||||||
/// `ChannelTx` `Drop` is a writer-notification only). Nothing the
|
/// `ChannelTx` `Drop` is a writer-notification only). Nothing the
|
||||||
/// seam drops ever messages the wire, so the report's send is the
|
/// seam drops ever messages the wire, so the report's send is the
|
||||||
@@ -195,20 +195,20 @@ impl AsyncWrite for GitChannelDuplex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The owned close-free read half — the seam-side reader. Semantics
|
/// The owned close-free read half, the seam-side reader. Semantics
|
||||||
/// repinned from russh's private `ChannelRx<S>` (the `into_stream`
|
/// repinned from russh's private `ChannelRx<S>` (the `into_stream`
|
||||||
/// shape's reader; identical observable behavior without the
|
/// shape's reader; identical observable behavior without the
|
||||||
/// `ChannelCloseOnDrop` wrapper): `Data` fills the buffer (partial
|
/// `ChannelCloseOnDrop` wrapper): `Data` fills the buffer (partial
|
||||||
/// reads buffered), `ExtendedData` with no sideband and the other
|
/// reads buffered), `ExtendedData` with no sideband and the other
|
||||||
/// non-data messages are swallowed and re-read (the `_` arm),
|
/// non-data messages are swallowed and re-read (the `_` arm),
|
||||||
/// `Eof` ends the stream, and the peer's `Close` leaves the reader
|
/// `Eof` ends the stream, and the peer's `Close` leaves the reader
|
||||||
/// to the channel's sender-death EOF (`wait()` → `None` — the
|
/// to the channel's sender-death EOF (`wait()` → `None`, the
|
||||||
/// channel's internal sender drops at the loop's close processing).
|
/// channel's internal sender dropping at the loop's close processing).
|
||||||
///
|
///
|
||||||
/// The `wait()` access is future-owned: each read round-trips the
|
/// The `wait()` access is future-owned: each read round-trips the
|
||||||
/// half through a boxed [`Future`] that parks it inside russh's
|
/// half through a boxed [`Future`] that parks it inside russh's
|
||||||
/// `ChannelReadHalf::wait` and hands it back with the observed
|
/// `ChannelReadHalf::wait` and hands it back with the observed
|
||||||
/// message on completion — the reader is fully owned (usable under
|
/// message on completion; the reader is fully owned (usable under
|
||||||
/// the seam's `'static` `T` bound) across every park, buffer and
|
/// the seam's `'static` `T` bound) across every park, buffer and
|
||||||
/// drop state; no `Drop` path ever messages the wire.
|
/// drop state; no `Drop` path ever messages the wire.
|
||||||
struct ChannelHalveReader {
|
struct ChannelHalveReader {
|
||||||
@@ -296,15 +296,15 @@ fn git_channel_duplex(channel: Channel<Msg>) -> GitChannelDuplex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The spawned git serving path: dispatch to the seam over the
|
/// The spawned git serving path: dispatch to the seam over the
|
||||||
/// [`git_channel_duplex`] (the split halves — the seam contracts
|
/// [`git_channel_duplex`] (the split halves; the seam contracts
|
||||||
/// nothing on its drops), then the exit-status-then-close report
|
/// nothing on its drops), then the exit-status-then-close report
|
||||||
/// through the cloned handle — the report's send is the sole
|
/// through the cloned handle; the report's send is the sole
|
||||||
/// close-contracting sender and each send awaits before the next is
|
/// close-contracting sender and each send awaits before the next is
|
||||||
/// issued, so the status-before-close order holds by construction
|
/// issued, so the status-before-close order holds by construction
|
||||||
/// under any schedule. The channel was taken from the handler's store
|
/// under any schedule. The channel was taken from the handler's store
|
||||||
/// by the dispatch (the store-and-take handoff); the spawned task owns
|
/// by the dispatch (the store-and-take handoff); the spawned task owns
|
||||||
/// the halves and their report surface. A failing report send means
|
/// the halves and their report surface. A failing report send means
|
||||||
/// the session is gone — the lifecycle's `warn`-failed records it (the
|
/// the session is gone; the lifecycle's `warn`-failed records it (the
|
||||||
/// one warn of the vocabulary).
|
/// one warn of the vocabulary).
|
||||||
pub(crate) async fn serve_git_row(
|
pub(crate) async fn serve_git_row(
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
|
|||||||
+147
-147
@@ -1,4 +1,4 @@
|
|||||||
//! The per-connection server handler — the ADR-004 dispositional auth
|
//! The per-connection server handler: the ADR-004 dispositional auth
|
||||||
//! bridge, plus the exec-grammar dispatch and the fail-closed decline
|
//! bridge, plus the exec-grammar dispatch and the fail-closed decline
|
||||||
//! core (`w2-dispatch-decline-core`; `w2-auth-bridge` filled the auth
|
//! core (`w2-dispatch-decline-core`; `w2-auth-bridge` filled the auth
|
||||||
//! hooks; `w3-git-mapping` replaced the skeleton probe with the full
|
//! hooks; `w3-git-mapping` replaced the skeleton probe with the full
|
||||||
@@ -9,30 +9,30 @@
|
|||||||
//!
|
//!
|
||||||
//! russh performs all cryptographic work: [`Handler::auth_publickey`]
|
//! russh performs all cryptographic work: [`Handler::auth_publickey`]
|
||||||
//! fires *after* signature and ownership verification, so this handler
|
//! fires *after* signature and ownership verification, so this handler
|
||||||
//! never sees or verifies a signature — its surface is dispositions
|
//! never sees or verifies a signature; its surface is dispositions
|
||||||
//! only. The single resolution path: ed25519-only gate → the crate's
|
//! only. The single resolution path: ed25519-only gate → the crate's
|
||||||
//! one fingerprint path ([`crate::fingerprint::fingerprint`]) → the
|
//! one fingerprint path ([`crate::fingerprint::fingerprint`]) → the
|
||||||
//! injected `IdentityProvider::resolve_from_fingerprint` → `Some`
|
//! injected `IdentityProvider::resolve_from_fingerprint` → `Some`
|
||||||
//! accepts with the resolved identity stored in per-connection handler
|
//! accepts with the resolved identity stored in per-connection handler
|
||||||
//! state (it rides the session for the wave-3 seam propagation —
|
//! state (it rides the session for the wave-3 seam propagation,
|
||||||
//! [`Handler::auth_succeeded`] is the ride-along hook); `None` rejects
|
//! [`Handler::auth_succeeded`] is the ride-along hook); `None` rejects
|
||||||
//! fail-closed — the resolved-identity-`None` path never creates a
|
//! fail-closed; the resolved-identity-`None` path never creates a
|
||||||
//! weakly authorized session.
|
//! weakly authorized session.
|
||||||
//!
|
//!
|
||||||
//! Every other method is an **explicit** reject — [`Handler::auth_none`],
|
//! Every other method is an **explicit** reject: [`Handler::auth_none`],
|
||||||
//! [`Handler::auth_password`] and [`Handler::auth_keyboard_interactive`]
|
//! [`Handler::auth_password`] and [`Handler::auth_keyboard_interactive`]
|
||||||
//! each implemented, never inherited — so the posture survives a pin
|
//! each implemented, never inherited, so the posture survives a pin
|
||||||
//! bump that could silently change russh's defaults (ADR-004 decision
|
//! bump that could silently change russh's defaults (ADR-004 decision
|
||||||
//! 3). The non-ed25519 algorithm decline is explicit-over-inherited too
|
//! 3). The non-ed25519 algorithm decline is explicit-over-inherited too
|
||||||
//! (ADR-002 decision 4). [`Handler::auth_publickey_offered`]
|
//! (ADR-002 decision 4). [`Handler::auth_publickey_offered`]
|
||||||
//! accepts-and-delegates explicitly (the probe hook consumes no attempt
|
//! accepts-and-delegates explicitly (the probe hook consumes no attempt,
|
||||||
//! — POC-1 verified; the delegation keeps `max_auth_attempts` counting
|
//! POC-1 verified; the delegation keeps `max_auth_attempts` counting
|
||||||
//! honest).
|
//! honest).
|
||||||
//!
|
//!
|
||||||
//! The username carries no semantics (OQ-03 resolved): resolution is
|
//! The username carries no semantics (OQ-03 resolved): resolution is
|
||||||
//! fingerprint-only — the bridge core takes no username parameter at
|
//! fingerprint-only; the bridge core takes no username parameter at
|
||||||
//! all, structurally refusing any identity input from the user string.
|
//! all, structurally refusing any identity input from the user string.
|
||||||
//! Auth declines are *dispositions* (`Auth::Reject`), not errors — the
|
//! Auth declines are *dispositions* (`Auth::Reject`), not errors; the
|
||||||
//! error model stays key-source-only.
|
//! error model stays key-source-only.
|
||||||
//!
|
//!
|
||||||
//! ## The parse-only dispatch + the fail-closed decline core
|
//! ## The parse-only dispatch + the fail-closed decline core
|
||||||
@@ -41,23 +41,23 @@
|
|||||||
//! exec-parse skeleton probe live in [`crate::dispatch`].) Every
|
//! exec-parse skeleton probe live in [`crate::dispatch`].) Every
|
||||||
//! `channel_open_*` hook gates per the feature graph: `session`
|
//! `channel_open_*` hook gates per the feature graph: `session`
|
||||||
//! channels open only where a session-serving feature exists (`session`
|
//! channels open only where a session-serving feature exists (`session`
|
||||||
//! ∨ `git` — the parse path lives inside session channels);
|
//! ∨ `git`, since the parse path lives inside session channels);
|
||||||
//! `direct-tcpip` / `forwarded-tcpip` / `direct-streamlocal` gate on
|
//! `direct-tcpip` / `forwarded-tcpip` / `direct-streamlocal` gate on
|
||||||
//! the `tunnel` feature; **`x11` always declines** — never
|
//! the `tunnel` feature; **`x11` always declines**, never
|
||||||
//! feature-gated (ADR-003 §5). Unfeatured and never-feature rows
|
//! feature-gated (ADR-003 §5). Unfeatured and never-feature rows
|
||||||
//! decline **at the open** with a clean wire reason
|
//! decline **at the open** with a clean wire reason
|
||||||
//! ([`crate::dispatch::unfeatured_open_decline`]) — never a
|
//! ([`crate::dispatch::unfeatured_open_decline`]), never a
|
||||||
//! half-open channel. The `direct-tcpip` / `direct-streamlocal`
|
//! half-open channel. The `direct-tcpip` / `direct-streamlocal`
|
||||||
//! featured rows are the route-1 bridge (`w3-tunnel-direct`,
|
//! featured rows are the route-1 bridge (`w3-tunnel-direct`,
|
||||||
//! [`crate::tunnel_map`]): the pre-admit decline classes (the unset
|
//! [`crate::tunnel_map`]): the pre-admit decline classes (the unset
|
||||||
//! fabric handle under the feature — the unset-declines posture; the
|
//! fabric handle under the feature, the unset-declines posture; the
|
||||||
//! unreachable-in-practice `no-identity`; an impossible wire target),
|
//! unreachable-in-practice `no-identity`; an impossible wire target),
|
||||||
//! the cap admission at the hook, then the spawned bridge — accept
|
//! the cap admission at the hook, then the spawned bridge; accept
|
||||||
//! only after the fabric open succeeds, `ConnectFailed` on failure
|
//! only after the fabric open succeeds, `ConnectFailed` on failure
|
||||||
//! ([`crate::dispatch::connect_failed_open_decline`], the wave-2
|
//! ([`crate::dispatch::connect_failed_open_decline`], the wave-2
|
||||||
//! seam stub's reason surviving as the real decline).
|
//! seam stub's reason surviving as the real decline).
|
||||||
//! An **inbound** `forwarded-tcpip` open — arriving *from* the vanilla
|
//! An **inbound** `forwarded-tcpip` open, arriving *from* the vanilla
|
||||||
//! client — declines with the same explicit `ConnectFailed` as the
|
//! client, declines with the same explicit `ConnectFailed` as the
|
||||||
//! unvanilla-arrival class (`w3-tunnel-listen`: the real `-R` data
|
//! unvanilla-arrival class (`w3-tunnel-listen`: the real `-R` data
|
||||||
//! leg is the door's own server-initiated opens, driven by the
|
//! leg is the door's own server-initiated opens, driven by the
|
||||||
//! [`crate::tunnel_map`] arrival pump; a vanilla client never
|
//! [`crate::tunnel_map`] arrival pump; a vanilla client never
|
||||||
@@ -65,47 +65,47 @@
|
|||||||
//! The bare
|
//! The bare
|
||||||
//! `auth-agent@openssh.com` channel *type*
|
//! `auth-agent@openssh.com` channel *type*
|
||||||
//! (`forwarded-streamlocal@openssh.com` likewise) has no russh handler
|
//! (`forwarded-streamlocal@openssh.com` likewise) has no russh handler
|
||||||
//! hook at this pin — russh rejects those opens inline
|
//! hook at this pin; russh rejects those opens inline
|
||||||
//! (administratively prohibited), which is the decline the never-
|
//! (administratively prohibited), which is the decline the never-
|
||||||
//! feature contract needs, but it is an inherited posture: a pin bump
|
//! feature contract needs, but it is an inherited posture: a pin bump
|
||||||
//! that changed it would surface here as an accepted-then-dropped
|
//! that changed it would surface here as an accepted-then-dropped
|
||||||
//! channel (third-party-upstream note; file upstream first).
|
//! channel (third-party-upstream note; file upstream first).
|
||||||
//!
|
//!
|
||||||
//! The session rows are **parse-only**: `exec_request` parses first
|
//! The session rows are **parse-only**: `exec_request` parses first
|
||||||
//! (the full grammar, [`crate::dispatch::parse_exec`] — a git route to
|
//! (the full grammar, [`crate::dispatch::parse_exec`], a git route to
|
||||||
//! the git row, a declined-with-reason grammar class to its honest
|
//! the git row, a declined-with-reason grammar class to its honest
|
||||||
//! decline, anything else to the session pipe row); `pty_request` /
|
//! decline, anything else to the session pipe row); `pty_request` /
|
||||||
//! `shell_request` route to the session rows. Nothing is served before
|
//! `shell_request` route to the session rows. Nothing is served before
|
||||||
//! a request exists — no request, no serving, so a request-less
|
//! a request exists: no request, no serving, so a request-less
|
||||||
//! session channel declines as unfeatured by construction.
|
//! session channel declines as unfeatured by construction.
|
||||||
//! **Decline first, alloc never**: every decline here precedes any
|
//! **Decline first, alloc never**: every decline here precedes any
|
||||||
//! channel/resource allocation (a leaked channel alloc is a
|
//! channel/resource allocation (a leaked channel alloc is a
|
||||||
//! capability leak — the POC-3 stage-A rule); the per-length
|
//! capability leak, the POC-3 stage-A rule); the per-length
|
||||||
//! `max_exec_command_len` cap is enforced before the parse itself.
|
//! `max_exec_command_len` cap is enforced before the parse itself.
|
||||||
//!
|
//!
|
||||||
//! The store-and-take channel handoff (`w3-git-mapping`)
|
//! The store-and-take channel handoff (`w3-git-mapping`)
|
||||||
//!
|
//!
|
||||||
//! A confirmed `session` open **stores** its [`Channel`] in
|
//! A confirmed `session` open **stores** its [`Channel`] in
|
||||||
//! per-connection handler state ([`Self::take_stored_channel`]'s
|
//! per-connection handler state ([`Self::take_stored_channel`]'s
|
||||||
//! supplier: `channel_open_session` accepts, then inserts — the
|
//! supplier: `channel_open_session` accepts, then inserts; the
|
||||||
//! POC-3 §A.4 accepted-but-held shape); the git row's `exec_request`
|
//! POC-3 §A.4 accepted-but-held shape); the git row's `exec_request`
|
||||||
//! **takes** it (`channels.remove(&id)` → `into_stream()` in the
|
//! **takes** it (`channels.remove(&id)` → `into_stream()` in the
|
||||||
//! spawned serving task) and the session's packet loop keeps owning
|
//! spawned serving task) and the session's packet loop keeps owning
|
||||||
//! the store: `channel_close` removes whatever it held (a channel
|
//! the store: `channel_close` removes whatever it held (a channel
|
||||||
//! that served or declined without an exec frees there), and every
|
//! that served or declined without an exec frees there), and every
|
||||||
//! mutation happens on the loop's task — single-writer, no lock. A
|
//! mutation happens on the loop's task: single-writer, no lock. A
|
||||||
//! git exec on a channel whose stored handle is already gone
|
//! git exec on a channel whose stored handle is already gone
|
||||||
//! (re-served, or never held — unvanilla) declines serving-path.
|
//! (re-served, or never held, which is unvanilla) declines serving-path.
|
||||||
//!
|
//!
|
||||||
//! ## The session rows' real serving paths (`w3-tty-session`)
|
//! ## The session rows' real serving paths (`w3-tty-session`)
|
||||||
//!
|
//!
|
||||||
//! The pty/shell/pipe rows replaced their wave-3 skeleton stubs with
|
//! The pty/shell/pipe rows replaced their wave-3 skeleton stubs with
|
||||||
//! the alktty translation bridge ([`crate::tty_map`]): the pty row
|
//! the alktty translation bridge ([`crate::tty_map`]): the pty row
|
||||||
//! stores the negotiated `TerminalParamsWire` ([`Self::ptys`]'s
|
//! stores the negotiated `TerminalParamsWire` ([`Self::ptys`]'s
|
||||||
//! supplier — taken by the later shell/exec request on the same
|
//! supplier, taken by the later shell/exec request on the same
|
||||||
//! channel, `ssh -t host "cmd"`'s pty-then-exec); the shell and pipe
|
//! channel, `ssh -t host "cmd"`'s pty-then-exec); the shell and pipe
|
||||||
//! rows take the stored channel the same way the git row does and
|
//! rows take the stored channel the same way the git row does and
|
||||||
//! spawn `serve_tty_row` over the configured seam — locally-built
|
//! spawn `serve_tty_row` over the configured seam, with locally-built
|
||||||
//! negotiation, unset-fields-decline (OQ-08: `shell_argv` unset →
|
//! negotiation, unset-fields-decline (OQ-08: `shell_argv` unset →
|
||||||
//! the shell row declines, `exec_wrapper_argv` unset → the pipe row
|
//! the shell row declines, `exec_wrapper_argv` unset → the pipe row
|
||||||
//! declines, `session_seam` unset → both rows and the pty row
|
//! declines, `session_seam` unset → both rows and the pty row
|
||||||
@@ -118,15 +118,15 @@
|
|||||||
//! ## The control rows' per-channel surface (`w3-tty-session`)
|
//! ## The control rows' per-channel surface (`w3-tty-session`)
|
||||||
//!
|
//!
|
||||||
//! `window_change_request` and `signal` arrive on the packet loop
|
//! `window_change_request` and `signal` arrive on the packet loop
|
||||||
//! **after** the serving task owns the wire — the spawned bridge is
|
//! **after** the serving task owns the wire, and the spawned bridge is
|
||||||
//! reached through [`Self::bridges`]' per-channel surface
|
//! reached through [`Self::bridges`]' per-channel surface
|
||||||
//! ([`crate::tty_map::BridgeSurface`]: the bounded control mpsc's
|
//! ([`crate::tty_map::BridgeSurface`]: the bounded control mpsc's
|
||||||
//! `try_send` — never awaited on the packet loop — plus the
|
//! `try_send` (never awaited on the packet loop), plus the
|
||||||
//! per-channel cancel). A resize for a channel whose pty is stored
|
//! per-channel cancel). A resize for a channel whose pty is stored
|
||||||
//! but not yet serving updates the stored params (the final size
|
//! but not yet serving updates the stored params (the final size
|
||||||
//! wins); signal names are accepted verbatim (the backend owns the
|
//! wins); signal names are accepted verbatim (the backend owns the
|
||||||
//! semantics). Unfeatured builds emit the request-level decline
|
//! semantics). Unfeatured builds emit the request-level decline
|
||||||
//! event with no reply — the control rows are fire-and-forget, no
|
//! event with no reply; the control rows are fire-and-forget, no
|
||||||
//! SSH reply exists to send. [`Self::channel_close`] releases the
|
//! SSH reply exists to send. [`Self::channel_close`] releases the
|
||||||
//! surface (firing the cancel: the alktty kill-on-drop contract is
|
//! surface (firing the cancel: the alktty kill-on-drop contract is
|
||||||
//! the cancelled session's cleanup) along with the channel and pty
|
//! the cancelled session's cleanup) along with the channel and pty
|
||||||
@@ -137,45 +137,45 @@
|
|||||||
//! ## The spawn-and-return structural constraint (ADR-006 decision 4)
|
//! ## The spawn-and-return structural constraint (ADR-006 decision 4)
|
||||||
//!
|
//!
|
||||||
//! russh at this pin runs handler callbacks **inline inside the
|
//! russh at this pin runs handler callbacks **inline inside the
|
||||||
//! session's packet loop** — a blocking handler suspends all packet
|
//! session's packet loop**: a blocking handler suspends all packet
|
||||||
//! routing and deadlocks against the peer's follow-up bytes (POC-3
|
//! routing and deadlocks against the peer's follow-up bytes (POC-3
|
||||||
//! proved by construction with receive-pack). Every serving path is
|
//! proved by construction with receive-pack). Every serving path is
|
||||||
//! therefore spawned through [`crate::serve::spawn_serving_path`] and
|
//! therefore spawned through [`crate::serve::spawn_serving_path`] and
|
||||||
//! the handler returns fast: the serving rows (the git row's alkgit
|
//! the handler returns fast: the serving rows (the git row's alkgit
|
||||||
//! dispatch, the session rows' config-driven declines and
|
//! dispatch, the session rows' config-driven declines and
|
||||||
//! [`crate::tty_map`]'s bridge) all clone `session.handle()`, spawn
|
//! [`crate::tty_map`]'s bridge) all clone `session.handle()`, spawn
|
||||||
//! through the clone, and are gone from the hook's stack — no hook
|
//! through the clone, and are gone from the hook's stack; no hook
|
||||||
//! awaits a serving path inline. The decline keeps its wire shape
|
//! awaits a serving path inline. The decline keeps its wire shape
|
||||||
//! (failure then close), riding the spawn. Exit/status reporting from
|
//! (failure then close), riding the spawn. Exit/status reporting from
|
||||||
//! spawned tasks rides the same cloned handle
|
//! spawned tasks rides the same cloned handle
|
||||||
//! ([`crate::serve::report_exit_status_then_close`] — the mechanics;
|
//! ([`crate::serve::report_exit_status_then_close`], the mechanics;
|
||||||
//! the tty rows' derivation rules live in [`crate::tty_map`]), pinned
|
//! the tty rows' derivation rules live in [`crate::tty_map`]), pinned
|
||||||
//! end-to-end by the mechanics rows in `tests/spawn_return.rs` and the
|
//! end-to-end by the mechanics rows in `tests/spawn_return.rs` and the
|
||||||
//! mapping rows in `tests/git_mapping.rs` / `tests/tty_mapping.rs`.
|
//! mapping rows in `tests/git_mapping.rs` / `tests/tty_mapping.rs`.
|
||||||
//!
|
//!
|
||||||
//! The open-level declines (unfeatured/never-feature rows, the
|
//! The open-level declines (unfeatured/never-feature rows, the
|
||||||
//! direct rows' pre-admit classes) stay inline by class: they answer
|
//! direct rows' pre-admit classes) stay inline by class: they answer
|
||||||
//! the open *before* any channel is owned — nothing is served yet,
|
//! the open *before* any channel is owned: nothing is served yet,
|
||||||
//! so nothing spawns. The route-1 bridge (`w3-tunnel-direct`) moved
|
//! so nothing spawns. The route-1 bridge (`w3-tunnel-direct`) moved
|
||||||
//! the direct rows' accept past the fabric open: the accept and the
|
//! the direct rows' accept past the fabric open: the accept and the
|
||||||
//! pump ride the spawned task, per the same constraint. The `-R`
|
//! pump ride the spawned task, per the same constraint. The `-R`
|
||||||
//! listen rows' family-ward dial is the one **inline await** on the
|
//! listen rows' family-ward dial is the one **inline await** on the
|
||||||
//! loop (`w3-tunnel-listen`'s recorded decision: the global request's
|
//! loop (`w3-tunnel-listen`'s recorded decision: the global request's
|
||||||
//! reply + the `&mut u32` bound-port out-param are the hook's own —
|
//! reply + the `&mut u32` bound-port out-param are the hook's own,
|
||||||
//! no spawned-task answer exists at this pin — and the fabric's
|
//! no spawned-task answer exists at this pin, and the fabric's
|
||||||
//! answer rides the fabric leg's connections, not this session's
|
//! answer rides the fabric leg's connections, not this session's
|
||||||
//! routing; bounded by `LISTEN_DIAL_TIMEOUT`), while everything under
|
//! routing; bounded by `LISTEN_DIAL_TIMEOUT`), while everything under
|
||||||
//! the reply — the arrivals, the pumps — stays spawned.
|
//! the reply (the arrivals, the pumps) stays spawned.
|
||||||
//!
|
//!
|
||||||
//! Request-level declines: `subsystem_request` declines every name —
|
//! Request-level declines: `subsystem_request` declines every name:
|
||||||
//! `sftp` is the OQ-13 placeholder (the feature exists, the route
|
//! `sftp` is the OQ-13 placeholder (the feature exists, the route
|
||||||
//! does not until alkfs lands); git service names carry only a name,
|
//! does not until alkfs lands); git service names carry only a name,
|
||||||
//! never a repository id, so git can never dispatch as a subsystem
|
//! never a repository id, so git can never dispatch as a subsystem
|
||||||
//! (ADR-006 decision 2); everything else is unimplemented. `env_request`
|
//! (ADR-006 decision 2); everything else is unimplemented. `env_request`
|
||||||
//! declines unconditionally (OQ-09 — no allowlist ships: wire env is
|
//! declines unconditionally (OQ-09; no allowlist ships: wire env is
|
||||||
//! not family config). `x11_request` and `agent_request` decline
|
//! not family config). `x11_request` and `agent_request` decline
|
||||||
//! (ADR-003 §5). The global listen requests (`tcpip_forward`
|
//! (ADR-003 §5). The global listen requests (`tcpip_forward`
|
||||||
//! / `streamlocal_forward` + their cancel forms) — the `-R` rows
|
//! / `streamlocal_forward` + their cancel forms), the `-R` rows
|
||||||
//! (`w3-tunnel-listen`): the real flow under `tunnel` with the listen
|
//! (`w3-tunnel-listen`): the real flow under `tunnel` with the listen
|
||||||
//! seam configured (the family-ward `TunnelListener::open` dial, the
|
//! seam configured (the family-ward `TunnelListener::open` dial, the
|
||||||
//! arrivals' arrival pump through the shared router); every decline
|
//! arrivals' arrival pump through the shared router); every decline
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
//! Two decline classes, chosen for vanilla-client observability (POC-1
|
//! Two decline classes, chosen for vanilla-client observability (POC-1
|
||||||
//! §5: channel-scoped decline, auth intact, no hangs):
|
//! §5: channel-scoped decline, auth intact, no hangs):
|
||||||
//!
|
//!
|
||||||
//! - **Open-level declines** — channel-open failure codes (`x11`
|
//! - **Open-level declines** (channel-open failure codes: `x11`
|
||||||
//! never-feature, unfeatured feature rows, the tunnel rows'
|
//! never-feature, unfeatured feature rows, the tunnel rows'
|
||||||
//! identity-less guard and beyond-cap refusals → administratively
|
//! identity-less guard and beyond-cap refusals → administratively
|
||||||
//! prohibited; the direct rows' cannot-bridge classes (the fabric
|
//! prohibited; the direct rows' cannot-bridge classes (the fabric
|
||||||
@@ -196,54 +196,54 @@
|
|||||||
//! connect failed). These
|
//! connect failed). These
|
||||||
//! happen before the channel exists, so the SSH open-failure code is
|
//! happen before the channel exists, so the SSH open-failure code is
|
||||||
//! the honest, richest reason surface.
|
//! the honest, richest reason surface.
|
||||||
//! - **Request-level declines** — [`Session::channel_failure`]: the
|
//! - **Request-level declines** ([`Session::channel_failure`]): the
|
||||||
//! SSH request-reply vocabulary has no failure text, so a request
|
//! SSH request-reply vocabulary has no failure text, so a request
|
||||||
//! decline's "reason" is the routing decision itself (documented
|
//! decline's "reason" is the routing decision itself (documented
|
||||||
//! above, in [`crate::dispatch`]'s grammar, and per feature build in
|
//! above, in [`crate::dispatch`]'s grammar, and per feature build in
|
||||||
//! `mapping.md`'s decline matrix). A *capability* decline answers
|
//! `mapping.md`'s decline matrix). A *capability* decline answers
|
||||||
//! failure and leaves the channel usable (the POC-1 §5
|
//! failure and leaves the channel usable (the POC-1 §5
|
||||||
//! session-survives-declines posture) — including the git grammar's
|
//! session-survives-declines posture), including the git grammar's
|
||||||
//! declinables (`unknown-service` / `service-only` /
|
//! declinables (`unknown-service` / `service-only` /
|
||||||
//! `invalid-repo-id`, the parsed reply shapes, `w3-git-mapping`);
|
//! `invalid-repo-id`, the parsed reply shapes, `w3-git-mapping`);
|
||||||
//! a *serving-path* decline (a routable row that owns the request's
|
//! a *serving-path* decline (a routable row that owns the request's
|
||||||
//! whole outcome: the seam-unconfigured git row, the session rows'
|
//! whole outcome: the seam-unconfigured git row, the session rows'
|
||||||
//! unset-field and unset-seam declines — `w3-tty-session`'s OQ-08
|
//! unset-field and unset-seam declines, `w3-tty-session`'s OQ-08
|
||||||
//! shapes — and the identity-less guard) answers failure and closes
|
//! shapes, and the identity-less guard) answers failure and closes
|
||||||
//! the channel — the vanilla-clean shape for a row whose serving
|
//! the channel, the vanilla-clean shape for a row whose serving
|
||||||
//! path cannot exist. The serving-path decline rides the spawned
|
//! path cannot exist. The serving-path decline rides the spawned
|
||||||
//! serving task (ADR-006 decision 4, §The spawn-and-return
|
//! serving task (ADR-006 decision 4, §The spawn-and-return
|
||||||
//! structural constraint above); the wire shape is unchanged. The
|
//! structural constraint above); the wire shape is unchanged. The
|
||||||
//! `channel-not-held` guard is the inline request-level shape (the
|
//! `channel-not-held` guard is the inline request-level shape (the
|
||||||
//! channel that could not be held stays usable). The real serve's
|
//! channel that could not be held stays usable). The real serve's
|
||||||
//! own outcomes are not declines: the substrate maps them (denial
|
//! own outcomes are not declines: the substrate maps them (denial
|
||||||
//! band before any protocol byte; exit-status then close — the
|
//! band before any protocol byte; exit-status then close (the
|
||||||
//! `exit_code` mapping decision recorded in [`crate::git_map`];
|
//! `exit_code` mapping decision recorded in [`crate::git_map`]);
|
||||||
//! the tty bridge's negotiation-error and exit-chunk decisions in
|
//! the tty bridge's negotiation-error and exit-chunk decisions in
|
||||||
//! [`crate::tty_map`]).
|
//! [`crate::tty_map`]).
|
||||||
//!
|
//!
|
||||||
//! ## The event vocabulary on every decline (`w3-decline-tracing`)
|
//! ## The event vocabulary on every decline (`w3-decline-tracing`)
|
||||||
//!
|
//!
|
||||||
//! Every decline site here emits its event through
|
//! Every decline site here emits its event through
|
||||||
//! [`crate::observe`] — the wave-2 gate's booked finding: the
|
//! [`crate::observe`], the wave-2 gate's booked finding: the
|
||||||
//! deciding context (row, feature build, channel id, decline class,
|
//! deciding context (row, feature build, channel id, decline class,
|
||||||
//! reason) rides the tracing event, not just the routing decision
|
//! reason) rides the tracing event, not just the routing decision
|
||||||
//! documented per build. The vocabulary and its safety posture (no
|
//! documented per build. The vocabulary and its safety posture (no
|
||||||
//! secrets, no username, no fingerprint — structurally) live in that
|
//! secrets, no username, no fingerprint; enforced structurally) live in that
|
||||||
//! module; the serving-path lifecycle (started / ended / failure)
|
//! module; the serving-path lifecycle (started / ended / failure)
|
||||||
//! hangs off the spawned stub body and is what the wave-3 mappings'
|
//! hangs off the spawned stub body and is what the wave-3 mappings'
|
||||||
//! real serving paths inherit.
|
//! real serving paths inherit.
|
||||||
//!
|
//!
|
||||||
//! ## Bounded resources at the handler surface (`w2-bounded-resources`)
|
//! ## Bounded resources at the handler surface (`w2-bounded-resources`)
|
||||||
//!
|
//!
|
||||||
//! (AGENTS.md convention 9; `overview.md` §Bounded-resources policy —
|
//! (AGENTS.md convention 9; `overview.md` §Bounded-resources policy,
|
||||||
//! the "concurrent channels per session — ours" row.) The in-crate
|
//! the "concurrent channels per session: ours" row.) The in-crate
|
||||||
//! concurrent-channel cap ([`crate::channel_cap::ChannelCap`]) is
|
//! concurrent-channel cap ([`crate::channel_cap::ChannelCap`]) is
|
||||||
//! enforced at every `channel_open_*` hook: an accept path beyond
|
//! enforced at every `channel_open_*` hook: an accept path beyond
|
||||||
//! `Limits::max_concurrent_channels` is refused fail-closed at the
|
//! `Limits::max_concurrent_channels` is refused fail-closed at the
|
||||||
//! open ([`crate::dispatch::cap_exceeded_open_decline`] — never
|
//! open ([`crate::dispatch::cap_exceeded_open_decline`]; never
|
||||||
//! queued, never half-open), and the count releases so the cap
|
//! queued, never half-open), and the count releases so the cap
|
||||||
//! recovers: through the `channel_close` hook for established
|
//! recovers: through the `channel_close` hook for established
|
||||||
//! channels, and — `w3-tunnel-direct` — inside the route-1 bridge's
|
//! channels, and, for `w3-tunnel-direct`, inside the route-1 bridge's
|
||||||
//! spawned task on a fabric-open failure (the admitted-but-rejected
|
//! spawned task on a fabric-open failure (the admitted-but-rejected
|
||||||
//! shape: ADR-006 decision 1 rejects the open, so no `channel_close`
|
//! shape: ADR-006 decision 1 rejects the open, so no `channel_close`
|
||||||
//! can ever fire for it; the release mechanism is the shared
|
//! can ever fire for it; the release mechanism is the shared
|
||||||
@@ -251,14 +251,14 @@
|
|||||||
//! its two budget surfaces (`w3-tunnel-listen`, recorded in
|
//! its two budget surfaces (`w3-tunnel-listen`, recorded in
|
||||||
//! `channel_cap.rs`): the arrivals admit into the owning route's
|
//! `channel_cap.rs`): the arrivals admit into the owning route's
|
||||||
//! count inside the spawned arrival tasks (a beyond-cap arrival never
|
//! count inside the spawned arrival tasks (a beyond-cap arrival never
|
||||||
//! opens an SSH channel — teardown-at-birth), and the listen count is
|
//! opens an SSH channel, which is teardown-at-birth), and the listen count is
|
||||||
//! its own dedicated counter against the same ceiling (the
|
//! its own dedicated counter against the same ceiling (the
|
||||||
//! listener/channels mix stays unconstrained; both stay bounded). The
|
//! listener/channels mix stays unconstrained; both stay bounded). The
|
||||||
//! order of the
|
//! order of the
|
||||||
//! gates: the feature gate first (an unfeatured build declines
|
//! gates: the feature gate first (an unfeatured build declines
|
||||||
//! everything before the cap matters), then the pure-decline classes
|
//! everything before the cap matters), then the pure-decline classes
|
||||||
//! (the tunnel rows' unset-handle/unix-gate/identity/invalid-target
|
//! (the tunnel rows' unset-handle/unix-gate/identity/invalid-target
|
||||||
//! checks precede the admission — decline-first-alloc-never applies
|
//! checks precede the admission; decline-first-alloc-never applies
|
||||||
//! to the budget too), the cap admission before any accept; the
|
//! to the budget too), the cap admission before any accept; the
|
||||||
//! admitted count is held across the fabric open and released exactly
|
//! admitted count is held across the fabric open and released exactly
|
||||||
//! once on either path. The `max_exec_command_len` cap is
|
//! once on either path. The `max_exec_command_len` cap is
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
//! The handler type is deliberately constructible outside this module
|
//! The handler type is deliberately constructible outside this module
|
||||||
//! (re-exported `#[doc(hidden)]` from the crate root) so the wave-1
|
//! (re-exported `#[doc(hidden)]` from the crate root) so the wave-1
|
||||||
//! contract tests drive these hooks directly against the in-memory
|
//! contract tests drive these hooks directly against the in-memory
|
||||||
//! doubles — the wire-facing `VerifiedPublicKey`/`PublicKeyOffer` hook
|
//! doubles; the wire-facing `VerifiedPublicKey`/`PublicKeyOffer` hook
|
||||||
//! arguments are `#[non_exhaustive]` (only russh constructs them), so
|
//! arguments are `#[non_exhaustive]` (only russh constructs them), so
|
||||||
//! the bridge core is its own method ([`SessionHandler::resolve_bridge`])
|
//! the bridge core is its own method ([`SessionHandler::resolve_bridge`])
|
||||||
//! the tests call with ordinary key material; the wire-level rows
|
//! the tests call with ordinary key material; the wire-level rows
|
||||||
@@ -308,36 +308,36 @@ use crate::tunnel_map;
|
|||||||
use alktty::negotiation::NegotiateRequest;
|
use alktty::negotiation::NegotiateRequest;
|
||||||
|
|
||||||
/// The per-connection handler (one per mounted stream): russh constructs
|
/// The per-connection handler (one per mounted stream): russh constructs
|
||||||
/// nothing here — [`crate::SshAdapter::handle`] builds one per accepted
|
/// nothing here; [`crate::SshAdapter::handle`] builds one per accepted
|
||||||
/// stream and drives it through `russh::server::run_stream`.
|
/// stream and drives it through `russh::server::run_stream`.
|
||||||
///
|
///
|
||||||
/// Carries the serve-config the auth bridge and dispatch consume, the
|
/// Carries the serve-config the auth bridge and dispatch consume, the
|
||||||
/// in-crate concurrent-channel cap (`ChannelCap` — see the private
|
/// in-crate concurrent-channel cap (`ChannelCap`, see the private
|
||||||
/// `channel_cap` module; enforced at every `channel_open_*` accept
|
/// `channel_cap` module; enforced at every `channel_open_*` accept
|
||||||
/// path, bounded resources, AGENTS.md convention 9), and the resolved
|
/// path, bounded resources, AGENTS.md convention 9), and the resolved
|
||||||
/// identity once a publickey auth has been accepted (see
|
/// identity once a publickey auth has been accepted (see
|
||||||
/// [`Self::resolved_identity`] — the wave-3 seam reads it).
|
/// [`Self::resolved_identity`], which the wave-3 seam reads).
|
||||||
pub struct SessionHandler {
|
pub struct SessionHandler {
|
||||||
serve: Arc<ServeConfig>,
|
serve: Arc<ServeConfig>,
|
||||||
/// The in-crate concurrent-channel cap (`w2-bounded-resources`),
|
/// The in-crate concurrent-channel cap (`w2-bounded-resources`),
|
||||||
/// shared as an `Arc` since `w3-tunnel-direct`: the route-1 bridge's
|
/// shared as an `Arc` since `w3-tunnel-direct`: the route-1 bridge's
|
||||||
/// spawned task releases the count on a fabric-open failure (the
|
/// spawned task releases the count on a fabric-open failure (the
|
||||||
/// admitted-but-rejected shape — no `channel_close` can ever fire
|
/// admitted-but-rejected shape, no `channel_close` can ever fire
|
||||||
/// for it; see `channel_cap.rs`'s release-sites record). Admissions
|
/// for it; see `channel_cap.rs`'s release-sites record). Admissions
|
||||||
/// have two sites since `w3-tunnel-listen` — the packet loop's
|
/// have two sites since `w3-tunnel-listen`: the packet loop's
|
||||||
/// open hooks and the `-R` arrival tasks — and releases three; the
|
/// open hooks and the `-R` arrival tasks, and releases three; the
|
||||||
/// mutex makes them sound (the reasoning, recorded in
|
/// mutex makes them sound (the reasoning, recorded in
|
||||||
/// `channel_cap.rs`).
|
/// `channel_cap.rs`).
|
||||||
channel_cap: Arc<ChannelCap>,
|
channel_cap: Arc<ChannelCap>,
|
||||||
/// The `-R` listen leg's per-session bookkeeping
|
/// The `-R` listen leg's per-session bookkeeping
|
||||||
/// (`w3-tunnel-listen`): the minted-listen map (token → pending
|
/// (`w3-tunnel-listen`): the minted-listen map (token → pending
|
||||||
/// listen; written only by the packet loop's hooks — the listen
|
/// listen; written only by the packet loop's hooks. The listen
|
||||||
/// global requests mints, the cancel forms + this handler's
|
/// global requests mints, the cancel forms + this handler's
|
||||||
/// teardown remove, single-writer/no-lock like the `channels`
|
/// teardown remove, single-writer/no-lock like the `channels`
|
||||||
/// store) and the listen count's dedicated
|
/// store) and the listen count's dedicated
|
||||||
/// `max_concurrent_channels`-ceiling counter (a listen is at least
|
/// `max_concurrent_channels`-ceiling counter (a listen is at least
|
||||||
/// one future channel's potential; its own counter keeps the
|
/// one future channel's potential; its own counter keeps the
|
||||||
/// listener/channels mix unconstrained while both stay bounded —
|
/// listener/channels mix unconstrained while both stay bounded,
|
||||||
/// no unbounded listener map).
|
/// no unbounded listener map).
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
listeners: HashMap<u32, tunnel_map::PendingListen>,
|
listeners: HashMap<u32, tunnel_map::PendingListen>,
|
||||||
@@ -349,7 +349,7 @@ pub struct SessionHandler {
|
|||||||
/// the close releases them. Written only by the packet loop's
|
/// the close releases them. Written only by the packet loop's
|
||||||
/// hooks (`channel_open_session` inserts; the git row's and the
|
/// hooks (`channel_open_session` inserts; the git row's and the
|
||||||
/// session rows' serving dispatches remove for their taken
|
/// session rows' serving dispatches remove for their taken
|
||||||
/// channel; `channel_close` removes) — single-writer, no lock.
|
/// channel; `channel_close` removes); single-writer, no lock.
|
||||||
channels: HashMap<ChannelId, Channel<Msg>>,
|
channels: HashMap<ChannelId, Channel<Msg>>,
|
||||||
/// The session rows' negotiated pty params (`w3-tty-session`):
|
/// The session rows' negotiated pty params (`w3-tty-session`):
|
||||||
/// `pty_request` stores, the later `shell_request`/`exec_request`
|
/// `pty_request` stores, the later `shell_request`/`exec_request`
|
||||||
@@ -376,17 +376,17 @@ impl Drop for SessionHandler {
|
|||||||
/// **session-row** serving task of this connection watches it and
|
/// **session-row** serving task of this connection watches it and
|
||||||
/// cancels its alktty session (the kill-on-drop cleanup) instead
|
/// cancels its alktty session (the kill-on-drop cleanup) instead
|
||||||
/// of outliving the connection. The git serving task does not
|
/// of outliving the connection. The git serving task does not
|
||||||
/// watch this signal — it ends through the channel duplex (stream
|
/// watch this signal; it ends through the channel duplex (stream
|
||||||
/// EOF / send failure) once the russh session dies, bounded by the
|
/// EOF / send failure) once the russh session dies, bounded by the
|
||||||
/// alkgit seam's own `session_wall_clock` (the alkgit limits the
|
/// alkgit seam's own `session_wall_clock` (the alkgit limits the
|
||||||
/// assembly configured). The `tunnel` build adds the `-R`
|
/// assembly configured). The `tunnel` build adds the `-R`
|
||||||
/// leg's teardown (`w3-tunnel-listen`): the session's pending
|
/// leg's teardown (`w3-tunnel-listen`): the session's pending
|
||||||
/// listens' tokens leave the shared router (a fabric arrival after
|
/// listens' tokens leave the shared router (a fabric arrival after
|
||||||
/// the session is gone is the correlation-miss class —
|
/// the session is gone is the correlation-miss class,
|
||||||
/// teardown-at-birth) and the listener objects drop here (the
|
/// teardown-at-birth) and the listener objects drop here (the
|
||||||
/// fabric-side reap). Any in-flight arrival already past the
|
/// fabric-side reap). Any in-flight arrival already past the
|
||||||
/// route lookup fails its dead-handle open and releases its
|
/// route lookup fails its dead-handle open and releases its
|
||||||
/// admission — the never-established path.
|
/// admission, the never-established path.
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
self.teardown.fire();
|
self.teardown.fire();
|
||||||
@@ -401,7 +401,7 @@ impl Drop for SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SessionHandler {
|
impl SessionHandler {
|
||||||
/// Builds the handler over the serving configuration — the
|
/// Builds the handler over the serving configuration; the
|
||||||
/// identity provider inside is the assembly-supplied
|
/// identity provider inside is the assembly-supplied
|
||||||
/// `alkcall::core::auth::IdentityProvider`; the bridge only
|
/// `alkcall::core::auth::IdentityProvider`; the bridge only
|
||||||
/// resolves through it.
|
/// resolves through it.
|
||||||
@@ -428,14 +428,14 @@ impl SessionHandler {
|
|||||||
/// store-and-take handoff): the git row's and the session rows'
|
/// store-and-take handoff): the git row's and the session rows'
|
||||||
/// dispatches remove the handle the open stored, handing the
|
/// dispatches remove the handle the open stored, handing the
|
||||||
/// stream into the spawned serving task. `None` when nothing is
|
/// stream into the spawned serving task. `None` when nothing is
|
||||||
/// held — a re-served or never-held channel (unvanilla; declined
|
/// held: a re-served or never-held channel (unvanilla; declined
|
||||||
/// serving-path).
|
/// serving-path).
|
||||||
#[cfg(any(feature = "git", feature = "session"))]
|
#[cfg(any(feature = "git", feature = "session"))]
|
||||||
fn take_stored_channel(&mut self, channel: ChannelId) -> Option<Channel<Msg>> {
|
fn take_stored_channel(&mut self, channel: ChannelId) -> Option<Channel<Msg>> {
|
||||||
self.channels.remove(&channel)
|
self.channels.remove(&channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The identity this connection currently rides — `Some` only after
|
/// The identity this connection currently rides, `Some` only after
|
||||||
/// a fingerprint resolution accepted a publickey auth (the bridge's
|
/// a fingerprint resolution accepted a publickey auth (the bridge's
|
||||||
/// fail-closed rejecting paths never leave a weakly-authorized
|
/// fail-closed rejecting paths never leave a weakly-authorized
|
||||||
/// state behind). The wave-3 seam propagation consumes this.
|
/// state behind). The wave-3 seam propagation consumes this.
|
||||||
@@ -447,15 +447,15 @@ impl SessionHandler {
|
|||||||
/// cap (bounded resources, `w2-bounded-resources`): room under
|
/// cap (bounded resources, `w2-bounded-resources`): room under
|
||||||
/// `Limits::max_concurrent_channels` admits and counts the channel;
|
/// `Limits::max_concurrent_channels` admits and counts the channel;
|
||||||
/// beyond the cap the open is refused fail-closed with a clean
|
/// beyond the cap the open is refused fail-closed with a clean
|
||||||
/// wire reason ([`crate::dispatch::cap_exceeded_open_decline`]) —
|
/// wire reason ([`crate::dispatch::cap_exceeded_open_decline`]),
|
||||||
/// never queued, never half-open. Called exactly at accept sites —
|
/// never queued, never half-open. Called exactly at accept sites:
|
||||||
/// the packet loop owns every admit call (the single admit writer,
|
/// the packet loop owns every admit call (the single admit writer,
|
||||||
/// [`ChannelCap`]'s recorded reasoning); a pure-decline path
|
/// [`ChannelCap`]'s recorded reasoning); a pure-decline path
|
||||||
/// (unfeatured, never-feature, the tunnel rows' pre-admit classes)
|
/// (unfeatured, never-feature, the tunnel rows' pre-admit classes)
|
||||||
/// never touches the count. Since `w3-tunnel-direct`, the tunnel
|
/// never touches the count. Since `w3-tunnel-direct`, the tunnel
|
||||||
/// rows admit **at the hook and hold the count across the fabric
|
/// rows admit **at the hook and hold the count across the fabric
|
||||||
/// open**: a fabric failure releases inside the spawned bridge
|
/// open**: a fabric failure releases inside the spawned bridge
|
||||||
/// task (the admitted-but-rejected shape — no `channel_close` can
|
/// task (the admitted-but-rejected shape; no `channel_close` can
|
||||||
/// ever fire); a success releases when the established channel
|
/// ever fire); a success releases when the established channel
|
||||||
/// closes ([`Self::channel_close`]). The release sites are disjoint
|
/// closes ([`Self::channel_close`]). The release sites are disjoint
|
||||||
/// per admission by the wire shape.
|
/// per admission by the wire shape.
|
||||||
@@ -467,16 +467,16 @@ impl SessionHandler {
|
|||||||
/// The ADR-004 decision 1 bridge core, structured out of the
|
/// The ADR-004 decision 1 bridge core, structured out of the
|
||||||
/// [`Handler::auth_publickey`] hook so the contract tests can drive
|
/// [`Handler::auth_publickey`] hook so the contract tests can drive
|
||||||
/// it directly: the hook's russh-owned argument shape
|
/// it directly: the hook's russh-owned argument shape
|
||||||
/// (`russh::server::VerifiedPublicKey`) is `#[non_exhaustive]` —
|
/// (`russh::server::VerifiedPublicKey`) is `#[non_exhaustive]`,
|
||||||
/// only russh constructs it — but this method takes the two facts
|
/// only russh constructs it, but this method takes the two facts
|
||||||
/// the hook receives (the verified key and the signature's
|
/// the hook receives (the verified key and the signature's
|
||||||
/// algorithm) as ordinary arguments.
|
/// algorithm) as ordinary arguments.
|
||||||
///
|
///
|
||||||
/// Ed25519-only gate first (ADR-002 decision 4 — `SkEd25519` and
|
/// Ed25519-only gate first (ADR-002 decision 4: `SkEd25519` and
|
||||||
/// every other scheme share the non-ed25519 decline, which is
|
/// every other scheme share the non-ed25519 decline, which is
|
||||||
/// explicit-over-inherited so it survives a pin bump), then the
|
/// explicit-over-inherited so it survives a pin bump), then the
|
||||||
/// single fingerprint path → the injected provider's
|
/// single fingerprint path → the injected provider's
|
||||||
/// `resolve_from_fingerprint`. No username parameter exists — the
|
/// `resolve_from_fingerprint`. No username parameter exists; the
|
||||||
/// user string is never an identity input (OQ-03). `None` is the
|
/// user string is never an identity input (OQ-03). `None` is the
|
||||||
/// fail-closed reject disposition; `Some` carries the identity the
|
/// fail-closed reject disposition; `Some` carries the identity the
|
||||||
/// hook stores in handler state.
|
/// hook stores in handler state.
|
||||||
@@ -503,7 +503,7 @@ impl SessionHandler {
|
|||||||
|
|
||||||
/// The `session` channel-open gate (ADR-003 feature graph):
|
/// The `session` channel-open gate (ADR-003 feature graph):
|
||||||
/// session channels open only where a session-serving feature
|
/// session channels open only where a session-serving feature
|
||||||
/// exists — `session` ∨ `git`, because git's parse path lives
|
/// exists: `session` ∨ `git`, because git's parse path lives
|
||||||
/// inside session channels.
|
/// inside session channels.
|
||||||
fn serves_session_channels(&self) -> bool {
|
fn serves_session_channels(&self) -> bool {
|
||||||
cfg!(feature = "session") || cfg!(feature = "git")
|
cfg!(feature = "session") || cfg!(feature = "git")
|
||||||
@@ -554,7 +554,7 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ADR-004 decision 2: accept-and-delegate, explicit — the probe
|
/// ADR-004 decision 2: accept-and-delegate, explicit; the probe
|
||||||
/// hook consumes no auth attempt (POC-1 verified), so keeping it at
|
/// hook consumes no auth attempt (POC-1 verified), so keeping it at
|
||||||
/// `Auth::Accept` keeps the `max_auth_attempts` counting honest;
|
/// `Auth::Accept` keeps the `max_auth_attempts` counting honest;
|
||||||
/// the real disposition is [`Self::auth_publickey`]'s.
|
/// the real disposition is [`Self::auth_publickey`]'s.
|
||||||
@@ -566,14 +566,14 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
Ok(Auth::Accept)
|
Ok(Auth::Accept)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ADR-004 decision 3: explicit family-contract decline — never the
|
/// ADR-004 decision 3: explicit family-contract decline, never the
|
||||||
/// inherited default (a pin bump could silently change it).
|
/// inherited default (a pin bump could silently change it).
|
||||||
async fn auth_none(&mut self, _user: &str) -> Result<Auth, Self::Error> {
|
async fn auth_none(&mut self, _user: &str) -> Result<Auth, Self::Error> {
|
||||||
observe::decline_auth("auth_none", "method-unimplemented");
|
observe::decline_auth("auth_none", "method-unimplemented");
|
||||||
Ok(Auth::reject())
|
Ok(Auth::reject())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ADR-004 decision 3: explicit family-contract decline — password
|
/// ADR-004 decision 3: explicit family-contract decline; password
|
||||||
/// auth does not exist on this door; ADR-005's key custody is the
|
/// auth does not exist on this door; ADR-005's key custody is the
|
||||||
/// whole story.
|
/// whole story.
|
||||||
async fn auth_password(&mut self, _user: &str, _password: &str) -> Result<Auth, Self::Error> {
|
async fn auth_password(&mut self, _user: &str, _password: &str) -> Result<Auth, Self::Error> {
|
||||||
@@ -581,7 +581,7 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
Ok(Auth::reject())
|
Ok(Auth::reject())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ADR-004 decision 3 + OQ-04 (resolved): explicit decline — the
|
/// ADR-004 decision 3 + OQ-04 (resolved): explicit decline, since the
|
||||||
/// CA-trust story does not exist in the family identity model;
|
/// CA-trust story does not exist in the family identity model;
|
||||||
/// future support is new-ADR territory.
|
/// future support is new-ADR territory.
|
||||||
async fn auth_openssh_certificate(
|
async fn auth_openssh_certificate(
|
||||||
@@ -604,16 +604,16 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
Ok(Auth::reject())
|
Ok(Auth::reject())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A counted channel closed — the in-crate concurrent-channel
|
/// A counted channel closed: the in-crate concurrent-channel
|
||||||
/// cap's decrement (`w2-bounded-resources`): every fired
|
/// cap's decrement (`w2-bounded-resources`): every fired
|
||||||
/// `channel_close` corresponds to exactly one previously-admitted
|
/// `channel_close` corresponds to exactly one previously-admitted
|
||||||
/// open (russh only routes `CHANNEL_CLOSE` for established
|
/// open (russh only routes `CHANNEL_CLOSE` for established
|
||||||
/// channels — a declined open never established one — and consumes
|
/// channels, a declined open never established one, and consumes
|
||||||
/// the close at most once), so the count recovers through this
|
/// the close at most once), so the count recovers through this
|
||||||
/// single release point, saturating in [`ChannelCap::release`] on
|
/// single release point, saturating in [`ChannelCap::release`] on
|
||||||
/// top. The spawned serving paths never touch the count directly:
|
/// top. The spawned serving paths never touch the count directly:
|
||||||
/// their closes ride the cloned handle, the client's close reply
|
/// their closes ride the cloned handle, the client's close reply
|
||||||
/// cycles back through the packet loop, and this hook fires — the
|
/// cycles back through the packet loop, and this hook fires; the
|
||||||
/// single-writer reasoning in [`ChannelCap`].
|
/// single-writer reasoning in [`ChannelCap`].
|
||||||
async fn channel_close(
|
async fn channel_close(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -634,15 +634,15 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
|
|
||||||
/// The session-channel open gate (ADR-003 feature graph,
|
/// The session-channel open gate (ADR-003 feature graph,
|
||||||
/// `session` ∨ `git`), then the in-crate concurrent-channel cap
|
/// `session` ∨ `git`), then the in-crate concurrent-channel cap
|
||||||
/// ([`Self::admit_channel_open`] — bounded resources,
|
/// ([`Self::admit_channel_open`], bounded resources,
|
||||||
/// `w2-bounded-resources`): unfeatured builds decline at the open
|
/// `w2-bounded-resources`): unfeatured builds decline at the open
|
||||||
/// — never a half-open channel — and a featured open beyond the
|
/// and never a half-open channel; a featured open beyond the
|
||||||
/// cap is refused fail-closed with a clean wire reason
|
/// cap is refused fail-closed with a clean wire reason
|
||||||
/// ([`crate::dispatch::cap_exceeded_open_decline`]), never queued.
|
/// ([`crate::dispatch::cap_exceeded_open_decline`]), never queued.
|
||||||
/// A confirmed open accepts the channel and counts it; the count
|
/// A confirmed open accepts the channel and counts it; the count
|
||||||
/// releases when the channel closes ([`Self::channel_close`]). The
|
/// releases when the channel closes ([`Self::channel_close`]). The
|
||||||
/// parse-only dispatch serves the accepted channel through its
|
/// parse-only dispatch serves the accepted channel through its
|
||||||
/// requests (nothing serves request-less — ADR-006 as amended).
|
/// requests (nothing serves request-less, per ADR-006 as amended).
|
||||||
async fn channel_open_session(
|
async fn channel_open_session(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel: Channel<Msg>,
|
channel: Channel<Msg>,
|
||||||
@@ -664,7 +664,7 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The x11 channel open: **always declines** — a never-feature
|
/// The x11 channel open: **always declines**, a never-feature
|
||||||
/// (ADR-003 §5), declined in every build, never feature-gated. The
|
/// (ADR-003 §5), declined in every build, never feature-gated. The
|
||||||
/// cap never applies here: a count is consumed only at an accept
|
/// cap never applies here: a count is consumed only at an accept
|
||||||
/// path, and this hook never has one.
|
/// path, and this hook never has one.
|
||||||
@@ -684,14 +684,14 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
/// The `direct-tcpip` open gate (`tunnel`) and its route-1 bridge
|
/// The `direct-tcpip` open gate (`tunnel`) and its route-1 bridge
|
||||||
/// dispatch (`w3-tunnel-direct`): unfeatured declines at the open;
|
/// dispatch (`w3-tunnel-direct`): unfeatured declines at the open;
|
||||||
/// featured rows build the bridge target from the SSH open
|
/// featured rows build the bridge target from the SSH open
|
||||||
/// arguments (`SubstrateAddr::HostPort` + `Substrate::Tcp` — an
|
/// arguments (`SubstrateAddr::HostPort` + `Substrate::Tcp`; an
|
||||||
/// out-of-range wire port is an impossible target, declined
|
/// out-of-range wire port is an impossible target, declined
|
||||||
/// pre-admission) and route to [`Self::dispatch_direct_open`]:
|
/// pre-admission) and route to [`Self::dispatch_direct_open`]:
|
||||||
/// pre-admit decline classes first (the unset fabric handle
|
/// pre-admit decline classes first (the unset fabric handle
|
||||||
/// declining under the feature — the unset-declines posture — and
|
/// declining under the feature, the unset-declines posture, and
|
||||||
/// the unreachable-in-practice `no-identity` guard), then the cap
|
/// the unreachable-in-practice `no-identity` guard), then the cap
|
||||||
/// admission **at the hook, held across the fabric open**, then
|
/// admission **at the hook, held across the fabric open**, then
|
||||||
/// the spawned bridge per spawn-and-return —
|
/// the spawned bridge per spawn-and-return:
|
||||||
/// `TunnelSession::open_direct` → accept only after the fabric
|
/// `TunnelSession::open_direct` → accept only after the fabric
|
||||||
/// open succeeds (`ConnectFailed` on failure, the count released
|
/// open succeeds (`ConnectFailed` on failure, the count released
|
||||||
/// in the task), `take_halves()` pumped against the taken channel.
|
/// in the task), `take_halves()` pumped against the taken channel.
|
||||||
@@ -737,7 +737,7 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The `forwarded-tcpip` open gate (`tunnel`): an **inbound** open
|
/// The `forwarded-tcpip` open gate (`tunnel`): an **inbound** open
|
||||||
/// of this channel type — arriving *from* the vanilla client — is
|
/// of this channel type (arriving *from* the vanilla client) is
|
||||||
/// not the `-R` leg at all (the `-R` data leg is the door's own
|
/// not the `-R` leg at all (the `-R` data leg is the door's own
|
||||||
/// server-initiated opens via [`crate::tunnel_map`]'s arrival
|
/// server-initiated opens via [`crate::tunnel_map`]'s arrival
|
||||||
/// pump; a vanilla client never initiates `forwarded-tcpip`,
|
/// pump; a vanilla client never initiates `forwarded-tcpip`,
|
||||||
@@ -769,10 +769,10 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
|
|
||||||
/// The `direct-streamlocal` open gate (`tunnel`) and its route-1
|
/// The `direct-streamlocal` open gate (`tunnel`) and its route-1
|
||||||
/// bridge dispatch: the `direct-tcpip` shape with the OQ-11
|
/// bridge dispatch: the `direct-tcpip` shape with the OQ-11
|
||||||
/// **runtime-Unix gate** — the row compiles wherever `tunnel`
|
/// **runtime-Unix gate**: the row compiles wherever `tunnel`
|
||||||
/// compiles (universal compile, no per-platform feature fork), but
|
/// compiles (universal compile, no per-platform feature fork), but
|
||||||
/// off-Unix it declines at the open **before any fabric call**
|
/// off-Unix it declines at the open **before any fabric call**
|
||||||
/// (never a partial bridge — the gate precedes the unset-handle
|
/// (never a partial bridge; the gate precedes the unset-handle
|
||||||
/// check, the identity guard, and the cap admission), and on-Unix
|
/// check, the identity guard, and the cap admission), and on-Unix
|
||||||
/// it rides the same bridge with `SubstrateAddr::Path` +
|
/// it rides the same bridge with `SubstrateAddr::Path` +
|
||||||
/// `Substrate::Unix`.
|
/// `Substrate::Unix`.
|
||||||
@@ -821,9 +821,9 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
|
|
||||||
/// The exec-grammar dispatch (ADR-006 point 3 as amended; the
|
/// The exec-grammar dispatch (ADR-006 point 3 as amended; the
|
||||||
/// full grammar, `w3-git-mapping`): the per-length cap checks the
|
/// full grammar, `w3-git-mapping`): the per-length cap checks the
|
||||||
/// wire bytes first (pre-parse, pre-allocation — decline first,
|
/// wire bytes first (pre-parse, pre-allocation; decline first,
|
||||||
/// alloc never), then [`crate::dispatch::parse_exec`] routes:
|
/// alloc never), then [`crate::dispatch::parse_exec`] routes:
|
||||||
/// a git route to the git row (feature-gated — a session-only
|
/// a git route to the git row (feature-gated; a session-only
|
||||||
/// build declines it as its gate), a grammar declinable to its
|
/// build declines it as its gate), a grammar declinable to its
|
||||||
/// honest request-level decline, anything else to the session
|
/// honest request-level decline, anything else to the session
|
||||||
/// pipe row. Invalid UTF-8 parses to the pipe row (parse-then-
|
/// pipe row. Invalid UTF-8 parses to the pipe row (parse-then-
|
||||||
@@ -889,11 +889,11 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
/// The `pty_request` row (`session`): the args →
|
/// The `pty_request` row (`session`): the args →
|
||||||
/// `TerminalParamsWire` (ttymodes → the reserved `{}` JSON,
|
/// `TerminalParamsWire` (ttymodes → the reserved `{}` JSON,
|
||||||
/// OQ-44), stored per channel for the later shell/exec request on
|
/// OQ-44), stored per channel for the later shell/exec request on
|
||||||
/// the same channel — after the seam check (a pty whose session
|
/// the same channel, after the seam check (a pty whose session
|
||||||
/// can never serve is a declined allocation,
|
/// can never serve is a declined allocation,
|
||||||
/// `seam-unconfigured`). Unfeatured builds decline the
|
/// `seam-unconfigured`). Unfeatured builds decline the
|
||||||
/// capability. The successful row answers `channel_success`
|
/// capability. The successful row answers `channel_success`
|
||||||
/// (POC-3 §D.1's shape — serving starts at the shell/exec
|
/// (POC-3 §D.1's shape: serving starts at the shell/exec
|
||||||
/// request).
|
/// request).
|
||||||
async fn pty_request(
|
async fn pty_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -930,11 +930,11 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
|
|
||||||
/// The `shell_request` row (`session`): the serve-config shell
|
/// The `shell_request` row (`session`): the serve-config shell
|
||||||
/// argv, verbatim, as `NegotiateRequest.cmd`; the stored pty
|
/// argv, verbatim, as `NegotiateRequest.cmd`; the stored pty
|
||||||
/// params consumed (`None` → pipe-mode shell — decided: a pty-less
|
/// params consumed (`None` → pipe-mode shell; decided: a pty-less
|
||||||
/// shell request is served, `ssh -T host` is legitimate vanilla);
|
/// shell request is served, `ssh -T host` is legitimate vanilla);
|
||||||
/// unset-declines first (OQ-08: `shell_argv` unset →
|
/// unset-declines first (OQ-08: `shell_argv` unset →
|
||||||
/// `shell-unconfigured`; the seam unset → `seam-unconfigured`
|
/// `shell-unconfigured`; the seam unset → `seam-unconfigured`
|
||||||
/// — both the spawned serving-path shape). A routed request
|
/// (both the spawned serving-path shape). A routed request
|
||||||
/// answers `channel_success` inline; the exit chunk reports the
|
/// answers `channel_success` inline; the exit chunk reports the
|
||||||
/// outcome, status-before-close from the spawned task.
|
/// outcome, status-before-close from the spawned task.
|
||||||
async fn shell_request(
|
async fn shell_request(
|
||||||
@@ -998,13 +998,13 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `window_change_request` (`session`): the resize control row —
|
/// `window_change_request` (`session`): the resize control row,
|
||||||
/// forwarded into the serving bridge's bounded control surface
|
/// forwarded into the serving bridge's bounded control surface
|
||||||
/// (`try_send`, never awaited on the packet loop); for a channel
|
/// (`try_send`, never awaited on the packet loop); for a channel
|
||||||
/// whose pty is stored but not yet serving, the stored params
|
/// whose pty is stored but not yet serving, the stored params
|
||||||
/// resize instead (the final size wins — vanilla sshd's own
|
/// resize instead (the final size wins, vanilla sshd's own
|
||||||
/// ordering); with neither, the event is dropped. Fire-and-forget
|
/// ordering); with neither, the event is dropped. Fire-and-forget
|
||||||
/// on the wire — no SSH reply exists, so a decline sends none
|
/// on the wire; no SSH reply exists, so a decline sends none
|
||||||
/// (the event is the record). Signal names: see [`Self::signal`].
|
/// (the event is the record). Signal names: see [`Self::signal`].
|
||||||
async fn window_change_request(
|
async fn window_change_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -1050,11 +1050,11 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `signal` row (`session`): accepted verbatim — no
|
/// The `signal` row (`session`): accepted verbatim, with no
|
||||||
/// alkssh-side name filter (the backend owns the semantics;
|
/// alkssh-side name filter (the backend owns the semantics;
|
||||||
/// alktty's control handle applies its own default kill for
|
/// alktty's control handle applies its own default kill for
|
||||||
/// names it rejects, and the pipe-mode pid-only vs PTY
|
/// names it rejects, and the pipe-mode pid-only vs PTY
|
||||||
/// process-group scope is backend semantics) — so there is no
|
/// process-group scope is backend semantics), so there is no
|
||||||
/// decline shape: the request is fire-and-forget on the SSH
|
/// decline shape: the request is fire-and-forget on the SSH
|
||||||
/// wire, and the bridge's bounded surface is the only path. An
|
/// wire, and the bridge's bounded surface is the only path. An
|
||||||
/// unfeatured build emits the decline event with no reply.
|
/// unfeatured build emits the decline event with no reply.
|
||||||
@@ -1082,7 +1082,7 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Every subsystem name declines (unimplemented on this door):
|
/// Every subsystem name declines (unimplemented on this door):
|
||||||
/// `subsystem("sftp")` is the OQ-13 placeholder — the feature
|
/// `subsystem("sftp")` is the OQ-13 placeholder; the feature
|
||||||
/// exists, the route does not until alkfs lands; git service
|
/// exists, the route does not until alkfs lands; git service
|
||||||
/// names carry only a name, never a repository id, so git can
|
/// names carry only a name, never a repository id, so git can
|
||||||
/// never dispatch as a subsystem (ADR-006 decision 2, POC-3
|
/// never dispatch as a subsystem (ADR-006 decision 2, POC-3
|
||||||
@@ -1101,7 +1101,7 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `env_request` declines unconditionally (OQ-09: no allowlist
|
/// `env_request` declines unconditionally (OQ-09: no allowlist
|
||||||
/// ships — wire env is not family config; the assembly supplies
|
/// ships, since wire env is not family config; the assembly supplies
|
||||||
/// the session env through [`crate::ServeConfig::session_env`]).
|
/// the session env through [`crate::ServeConfig::session_env`]).
|
||||||
async fn env_request(
|
async fn env_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -1132,7 +1132,7 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Agent forwarding on a session session-channel: always declines
|
/// Agent forwarding on a session session-channel: always declines
|
||||||
/// (never-feature, ADR-003 §5 — agent forwarding is the
|
/// (never-feature, ADR-003 §5; agent forwarding is the
|
||||||
/// anti-pattern of the alkvault custody model). `false` is the
|
/// anti-pattern of the alkvault custody model). `false` is the
|
||||||
/// decline; russh replies the channel failure.
|
/// decline; russh replies the channel failure.
|
||||||
async fn agent_request(
|
async fn agent_request(
|
||||||
@@ -1146,7 +1146,7 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
|
|
||||||
/// The `tcpip-forward` listen request (`-R` ingress, `w3-tunnel-
|
/// The `tcpip-forward` listen request (`-R` ingress, `w3-tunnel-
|
||||||
/// listen`): the gate is [`Self::serves_tunnels`]; the real flow
|
/// listen`): the gate is [`Self::serves_tunnels`]; the real flow
|
||||||
/// rides [`listen_request_dispatch`] (`tunnel` builds) — the
|
/// rides [`listen_request_dispatch`] (`tunnel` builds), the
|
||||||
/// family-ward `TunnelListener::open` dial, the bound-port out-param
|
/// family-ward `TunnelListener::open` dial, the bound-port out-param
|
||||||
/// (`port = 0` → the fabric's actually-bound number, the sshd
|
/// (`port = 0` → the fabric's actually-bound number, the sshd
|
||||||
/// parity), the route + listener registration. Unfeatured builds
|
/// parity), the route + listener registration. Unfeatured builds
|
||||||
@@ -1179,13 +1179,13 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The `cancel-tcpip-forward` cancel form (`-KR`, mux teardown,
|
/// The `cancel-tcpip-forward` cancel form (`-KR`, mux teardown,
|
||||||
/// `ExitOnForwardFailure`): 1:1 into the family listener's teardown —
|
/// `ExitOnForwardFailure`): 1:1 into the family listener's teardown,
|
||||||
/// the pending listen matched by (the wire address as the request
|
/// the pending listen matched by (the wire address as the request
|
||||||
/// carried it, the bound port), the route removed **before** any
|
/// carried it, the bound port), the route removed **before** any
|
||||||
/// teardown (a racing arrival is already the correlation-miss
|
/// teardown (a racing arrival is already the correlation-miss
|
||||||
/// class), the listener count released, the fabric channel's
|
/// class), the listener count released, the fabric channel's
|
||||||
/// teardown spawned (`TunnelListener::close`). `false` for a
|
/// teardown spawned (`TunnelListener::close`). `false` for a
|
||||||
/// (address, port) pair with no pending listen — the sshd parity
|
/// (address, port) pair with no pending listen; the sshd parity
|
||||||
/// (`cancel of a not-found forward`).
|
/// (`cancel of a not-found forward`).
|
||||||
async fn cancel_tcpip_forward(
|
async fn cancel_tcpip_forward(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -1206,7 +1206,7 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The `streamlocal-forward` listen request: the same listen flow
|
/// The `streamlocal-forward` listen request: the same listen flow
|
||||||
/// as `tcpip_forward` over the unix substrate — with the OQ-11
|
/// as `tcpip_forward` over the unix substrate, with the OQ-11
|
||||||
/// runtime-Unix gate riding the serving flow it serves (off-Unix
|
/// runtime-Unix gate riding the serving flow it serves (off-Unix
|
||||||
/// it declines before any fabric call; universal compile, no
|
/// it declines before any fabric call; universal compile, no
|
||||||
/// per-platform feature fork). No port out-param (the unix listen
|
/// per-platform feature fork). No port out-param (the unix listen
|
||||||
@@ -1271,34 +1271,34 @@ impl russh::server::Handler for SessionHandler {
|
|||||||
/// gate order is decline-first-alloc-never, each consumer checked
|
/// gate order is decline-first-alloc-never, each consumer checked
|
||||||
/// before the next allocation is taken:
|
/// before the next allocation is taken:
|
||||||
///
|
///
|
||||||
/// 1. **The `no-identity` guard** — unreachable on the wire (russh
|
/// 1. **The `no-identity` guard**: unreachable on the wire (russh
|
||||||
/// authorizes before requests serve) and fail-closed: never
|
/// authorizes before requests serve) and fail-closed: never
|
||||||
/// anonymous listens.
|
/// anonymous listens.
|
||||||
/// 2. **The seam checks** — the fabric handle or the listen seam
|
/// 2. **The seam checks**: the fabric handle or the listen seam
|
||||||
/// (`listen_router` absent: an egress-only door — the seam unset or
|
/// (`listen_router` absent: an egress-only door, whether the seam unset or
|
||||||
/// the registration never ran) decline `seam-unconfigured` before
|
/// the registration never ran) decline `seam-unconfigured` before
|
||||||
/// any dial.
|
/// any dial.
|
||||||
/// 3. **The OQ-11 unix gate** — the streamlocal variant's off-Unix
|
/// 3. **The OQ-11 unix gate**: the streamlocal variant's off-Unix
|
||||||
/// decline rides its hook (checked before this fn runs, so the
|
/// decline rides its hook (checked before this fn runs, so the
|
||||||
/// unreachable-on-Unix-only row never here).
|
/// unreachable-on-Unix-only row never here).
|
||||||
/// 4. **The listener cap** — the listen count's dedicated
|
/// 4. **The listener cap**: the listen count's dedicated
|
||||||
/// `max_concurrent_channels`-ceiling admission; beyond it
|
/// `max_concurrent_channels`-ceiling admission; beyond it
|
||||||
/// `cap-exceeded`.
|
/// `cap-exceeded`.
|
||||||
///
|
///
|
||||||
/// Then the family-ward dial ([`crate::tunnel_map::open_listen`], the
|
/// Then the family-ward dial ([`crate::tunnel_map::open_listen`], the
|
||||||
/// `resource = <the wire bind endpoint>` naming convention) — awaited
|
/// `resource = <the wire bind endpoint>` naming convention), awaited
|
||||||
/// inline (the recorded inline decision: the global request's reply +
|
/// inline (the recorded inline decision: the global request's reply +
|
||||||
/// the `&mut u32` out-param are the hook's; the fabric answer rides
|
/// the `&mut u32` out-param are the hook's; the fabric answer rides
|
||||||
/// the fabric leg, not this session's packet loop; the dial is
|
/// the fabric leg, not this session's packet loop; the dial is
|
||||||
/// bounded by `LISTEN_DIAL_TIMEOUT`, so the stall is bounded). The
|
/// bounded by `LISTEN_DIAL_TIMEOUT`, so the stall is bounded). The
|
||||||
/// reply's `bound` port writes the out-param (`port = 0` → the
|
/// reply's `bound` port writes the out-param (`port = 0` → the
|
||||||
/// actually-bound number, sshd parity); the route registers into the
|
/// actually-bound number, sshd parity); the route registers into the
|
||||||
/// shared router — the register is the collision pre-check
|
/// shared router; the register is the collision pre-check
|
||||||
/// (`w4-router-collision`): a mint on an already-held token refuses
|
/// (`w4-router-collision`): a mint on an already-held token refuses
|
||||||
/// fail-closed (`token-collision`, the minted listener torn down, the
|
/// fail-closed (`token-collision`, the minted listener torn down, the
|
||||||
/// cap released — a collided mint can never steal a live route; the
|
/// cap released, so a collided mint can never steal a live route; the
|
||||||
/// decline rides `Ok(false)`, never the error taxonomy, since a
|
/// decline rides `Ok(false)`, never the error taxonomy, since a
|
||||||
/// global-request hook's `Err` would kill the whole session) — and
|
/// global-request hook's `Err` would kill the whole session), and
|
||||||
/// the pending listen into the session's
|
/// the pending listen into the session's
|
||||||
/// packet-loop-only map. `Ok(true)`'s answer is the only success
|
/// packet-loop-only map. `Ok(true)`'s answer is the only success
|
||||||
/// shape; every decline path answers the vanilla client's
|
/// shape; every decline path answers the vanilla client's
|
||||||
@@ -1400,15 +1400,15 @@ async fn listen_request_dispatch(
|
|||||||
/// listener's teardown. Match rule (sshd parity): the wire endpoint as
|
/// listener's teardown. Match rule (sshd parity): the wire endpoint as
|
||||||
/// the original request carried it + the bound port for the tcp
|
/// the original request carried it + the bound port for the tcp
|
||||||
/// variants (the client sends the same normalized string it sent
|
/// variants (the client sends the same normalized string it sent
|
||||||
/// originally, and the *allocated* port for `port = 0` mints — the
|
/// originally, and the *allocated* port for `port = 0` mints, the
|
||||||
/// value the listen reply carried); the path alone for the unix
|
/// value the listen reply carried); the path alone for the unix
|
||||||
/// variant. Order: the entry leaves the session map and the route
|
/// variant. Order: the entry leaves the session map and the route
|
||||||
/// leaves the shared router before any family teardown spawns (a
|
/// leaves the shared router before any family teardown spawns (a
|
||||||
/// racing arrival that has not yet cloned its route is already the
|
/// racing arrival that has not yet cloned its route is already the
|
||||||
/// correlation-miss class — no post-cancel half-serve; one that
|
/// correlation-miss class, so no post-cancel half-serve; one that
|
||||||
/// cloned just before completes independently — the recorded
|
/// cloned just before completes independently (the recorded
|
||||||
/// caveat), then the listener count releases, then the family
|
/// caveat), then the listener count releases, then the family
|
||||||
/// teardown spawns (`TunnelListener::close` inside the spawn — a
|
/// teardown spawns (`TunnelListener::close` inside the spawn, a
|
||||||
/// bounded local shutdown + entry reap, not a serving path the hook
|
/// bounded local shutdown + entry reap, not a serving path the hook
|
||||||
/// may await). `Ok(true)` iff a pending listen matched.
|
/// may await). `Ok(true)` iff a pending listen matched.
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
@@ -1449,7 +1449,7 @@ fn cancel_listen_dispatch(
|
|||||||
|
|
||||||
/// The request-level decline shape for an unfeatured/unimplemented
|
/// The request-level decline shape for an unfeatured/unimplemented
|
||||||
/// capability: fail the request and leave the channel usable (POC-1
|
/// capability: fail the request and leave the channel usable (POC-1
|
||||||
/// §5 — session survives declines, no state poisoning). The decline
|
/// §5: session survives declines, no state poisoning). The decline
|
||||||
/// event is emitted by the caller (the hook carries the row/reason
|
/// event is emitted by the caller (the hook carries the row/reason
|
||||||
/// context); this is the wire shape only.
|
/// context); this is the wire shape only.
|
||||||
async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
|
async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
|
||||||
@@ -1459,16 +1459,16 @@ async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
|
|||||||
/// The spawned serving-path decline body (`w3-git-mapping`'s shape,
|
/// The spawned serving-path decline body (`w3-git-mapping`'s shape,
|
||||||
/// reused by `w3-tty-session`'s config-driven declines): fail the
|
/// reused by `w3-tty-session`'s config-driven declines): fail the
|
||||||
/// request and close the channel, through the cloned handle the task
|
/// request and close the channel, through the cloned handle the task
|
||||||
/// was handed — the vanilla-clean shape for a routed row that owns
|
/// was handed, the vanilla-clean shape for a routed row that owns
|
||||||
/// the request's whole outcome. The helper name keeps the wave-3
|
/// the request's whole outcome. The helper name keeps the wave-3
|
||||||
/// history (the decline IS the path it spawns). Decline-first-alloc-
|
/// history (the decline IS the path it spawns). Decline-first-alloc-
|
||||||
/// never holds structurally: nothing is allocated before the failure
|
/// never holds structurally: nothing is allocated before the failure
|
||||||
/// lands — the spawn's handle clone is the only resource ever taken.
|
/// lands; the spawn's handle clone is the only resource ever taken.
|
||||||
///
|
///
|
||||||
/// The serving-path lifecycle events ride this body
|
/// The serving-path lifecycle events ride this body
|
||||||
/// ([`crate::observe`]): started (the task's first action), the
|
/// ([`crate::observe`]): started (the task's first action), the
|
||||||
/// serving-path decline (class `serving`, the caller's vocabulary
|
/// serving-path decline (class `serving`, the caller's vocabulary
|
||||||
/// reason), and — after the wire reporting has completed — ended, or
|
/// reason), and, after the wire reporting has completed, ended, or
|
||||||
/// failed when a report send could not complete (the one warn in the
|
/// failed when a report send could not complete (the one warn in the
|
||||||
/// vocabulary).
|
/// vocabulary).
|
||||||
#[cfg(any(feature = "git", feature = "session"))]
|
#[cfg(any(feature = "git", feature = "session"))]
|
||||||
@@ -1493,7 +1493,7 @@ async fn seam_stub_decline(
|
|||||||
|
|
||||||
/// Spawns a serving-path decline as the row's serving path (ADR-006
|
/// Spawns a serving-path decline as the row's serving path (ADR-006
|
||||||
/// decision 4): the handle is cloned here, the decline runs on its own
|
/// decision 4): the handle is cloned here, the decline runs on its own
|
||||||
/// task, and the hook returns — no serving path awaits inline. The
|
/// task, and the hook returns; no serving path awaits inline. The
|
||||||
/// row never serves, so the decline *is* the path: failure + close,
|
/// row never serves, so the decline *is* the path: failure + close,
|
||||||
/// event-shaped ([`crate::observe`]) with the caller's reason.
|
/// event-shaped ([`crate::observe`]) with the caller's reason.
|
||||||
#[cfg(any(feature = "git", feature = "session"))]
|
#[cfg(any(feature = "git", feature = "session"))]
|
||||||
@@ -1509,14 +1509,14 @@ fn spawn_seam_stub(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The git row's routing stage (feature-gated, `w3-git-mapping`):
|
/// The git row's routing stage (feature-gated, `w3-git-mapping`):
|
||||||
/// validation of the normalized id ([`crate::git_map::repo_id_validates`] —
|
/// validation of the normalized id ([`crate::git_map::repo_id_validates`],
|
||||||
/// reply-shaped `invalid-repo-id`), the seam-unconfigured decline
|
/// reply-shaped `invalid-repo-id`), the seam-unconfigured decline
|
||||||
/// ([`Self::serves_git_row`] alone configures nothing — the
|
/// ([`Self::serves_git_row`] alone configures nothing, per the
|
||||||
/// unset-declines contract), then the store-and-take handoff and the
|
/// unset-declines contract), then the store-and-take handoff and the
|
||||||
/// spawned alkgit dispatch ([`crate::git_map::serve_git_row`], the
|
/// spawned alkgit dispatch ([`crate::git_map::serve_git_row`], the
|
||||||
/// resolved identity riding along). A routed exec answers
|
/// resolved identity riding along). A routed exec answers
|
||||||
/// `channel_success` inline (the request-reply vocabulary: the request
|
/// `channel_success` inline (the request-reply vocabulary: the request
|
||||||
/// routed; the serving outcome reports through exit-status later —
|
/// routed; the serving outcome reports through exit-status later,
|
||||||
/// the POC-3 §A.5 shape), never awaited serving.
|
/// the POC-3 §A.5 shape), never awaited serving.
|
||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
fn git_row_dispatch(
|
fn git_row_dispatch(
|
||||||
@@ -1569,12 +1569,12 @@ fn git_row_dispatch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The session pipe row's dispatch (`w3-tty-session`): unset-declines
|
/// The session pipe row's dispatch (`w3-tty-session`): unset-declines
|
||||||
/// first (OQ-08 — `exec_wrapper_argv` unset → `wrapper-unconfigured`;
|
/// first (OQ-08: `exec_wrapper_argv` unset → `wrapper-unconfigured`;
|
||||||
/// the seam unset → `seam-unconfigured`; both the spawned serving-path
|
/// the seam unset → `seam-unconfigured`; both the spawned serving-path
|
||||||
/// shape, never a fallback), then the store-and-take handoff
|
/// shape, never a fallback), then the store-and-take handoff
|
||||||
/// (`channel-not-held` → the inline unvanilla decline, the git row's
|
/// (`channel-not-held` → the inline unvanilla decline, the git row's
|
||||||
/// shape), the wire command appended to the wrapper argv (`wrapper ++
|
/// shape), the wire command appended to the wrapper argv (`wrapper ++
|
||||||
/// [cmd]` — never naively split), the stored pty params consumed if a
|
/// [cmd]`, never naively split), the stored pty params consumed if a
|
||||||
/// pty preceded, and the spawned bridge. A routed exec answers
|
/// pty preceded, and the spawned bridge. A routed exec answers
|
||||||
/// `channel_success` inline (the request routed; the exit chunk
|
/// `channel_success` inline (the request routed; the exit chunk
|
||||||
/// reports the serving outcome later).
|
/// reports the serving outcome later).
|
||||||
@@ -1637,7 +1637,7 @@ fn pipe_row_dispatch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Installs the per-channel bridge surface, clones the session
|
/// Installs the per-channel bridge surface, clones the session
|
||||||
/// handle, and spawns the spawned serving path — the session rows'
|
/// handle, and spawns the serving path, the session rows'
|
||||||
/// spawn step (ADR-006 decision 4: the handler returns fast; the
|
/// spawn step (ADR-006 decision 4: the handler returns fast; the
|
||||||
/// routed request answers `channel_success` inline).
|
/// routed request answers `channel_success` inline).
|
||||||
#[cfg(feature = "session")]
|
#[cfg(feature = "session")]
|
||||||
@@ -1674,24 +1674,24 @@ fn spawn_tty_row(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The direct tunnel rows' dispatch (`w3-tunnel-direct`): the
|
/// The direct tunnel rows' dispatch (`w3-tunnel-direct`): the
|
||||||
/// pre-admit decline classes first — **decline first, alloc never**
|
/// pre-admit decline classes first, because **decline first, alloc
|
||||||
/// applies to the cap budget too, so every consumer is checked before
|
/// never** applies to the cap budget too, so every consumer is checked before
|
||||||
/// the admission is taken. Order (the recorded admission mechanics:
|
/// the admission is taken. Order (the recorded admission mechanics:
|
||||||
/// the admission is taken at the hook — the packet-loop's single
|
/// the admission is taken at the hook, the packet-loop's single
|
||||||
/// admit-writer site, no race window against the cap check — and held
|
/// admit-writer site, no race window against the cap check, and held
|
||||||
/// across the fabric open):
|
/// across the fabric open):
|
||||||
///
|
///
|
||||||
/// 1. **The unset fabric handle decline** (`seam-unconfigured`, the
|
/// 1. **The unset fabric handle decline** (`seam-unconfigured`, the
|
||||||
/// unset-declines posture — the feature alone configures nothing):
|
/// unset-declines posture, the feature alone configures nothing):
|
||||||
/// `ConnectFailed` ([`crate::dispatch::connect_failed_open_decline`])
|
/// `ConnectFailed` ([`crate::dispatch::connect_failed_open_decline`])
|
||||||
/// — the bridge cannot be attempted at all.
|
/// as the bridge cannot be attempted at all.
|
||||||
/// 2. **The `no-identity` guard** — unreachable on the wire (russh
|
/// 2. **The `no-identity` guard**: unreachable on the wire (russh
|
||||||
/// authorizes before channels exist) and fail-closed: never
|
/// authorizes before channels exist) and fail-closed: never
|
||||||
/// anonymous serving (the git/tty rows' shape, discharged here for
|
/// anonymous serving (the git/tty rows' shape, discharged here for
|
||||||
/// the tunnel row — the parked `w2-auth-bridge` item; the
|
/// the tunnel row, the parked `w2-auth-bridge` item; the
|
||||||
/// `set_identity` mechanics the rides records live in
|
/// `set_identity` mechanics the rides records live in
|
||||||
/// [`crate::tunnel_map`]).
|
/// [`crate::tunnel_map`]).
|
||||||
/// 3. **The impossible-target decline** (`invalid-target` — an
|
/// 3. **The impossible-target decline** (`invalid-target`, an
|
||||||
/// out-of-range wire port): `ConnectFailed`.
|
/// out-of-range wire port): `ConnectFailed`.
|
||||||
///
|
///
|
||||||
/// Then the cap admission ([`Self::admit_channel_open`]; beyond it →
|
/// Then the cap admission ([`Self::admit_channel_open`]; beyond it →
|
||||||
|
|||||||
+15
-15
@@ -1,27 +1,27 @@
|
|||||||
//! The client-side host-key policy seam (OQ-05 resolution) — the
|
//! The client-side host-key policy seam (OQ-05 resolution): the
|
||||||
//! trust-decision contract the dial surface calls at russh's
|
//! trust-decision contract the dial surface calls at russh's
|
||||||
//! `check_server_key` hook.
|
//! `check_server_key` hook.
|
||||||
//!
|
//!
|
||||||
//! Two cases, one seam — the crate is **policy-blind**
|
//! Two cases, one seam: the crate is **policy-blind**
|
||||||
//! (`docs/architecture/auth.md` §Host-key policy):
|
//! (`docs/architecture/auth.md` §Host-key policy):
|
||||||
//!
|
//!
|
||||||
//! 1. **Known beforehand** (trust anchors exist; family-built runners
|
//! 1. **Known beforehand** (trust anchors exist; family-built runners
|
||||||
//! mint their host key at assembly time): assembly-supplied pins
|
//! mint their host key at assembly time): assembly-supplied pins
|
||||||
//! with **hard decline on mismatch**. The crate never hard-codes
|
//! with **hard decline on mismatch**. The crate never hard-codes
|
||||||
//! this — a pin policy is a [`HostKeyPolicy`] implementation the
|
//! this; a pin policy is a [`HostKeyPolicy`] implementation the
|
||||||
//! assembly writes, backed by its own pin store (the host *pin* is
|
//! assembly writes, backed by its own pin store (the host *pin* is
|
||||||
//! public config, ADR-005 decision 6; never vault material).
|
//! public config, ADR-005 decision 6; never vault material).
|
||||||
//! 2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no
|
//! 2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no
|
||||||
//! anchor until first contact): prompt-and-record,
|
//! anchor until first contact): prompt-and-record,
|
||||||
//! provider-metadata match, learn-and-record, or accept-and-
|
//! provider-metadata match, learn-and-record, or accept-and-
|
||||||
//! continue (the ephemeral-fleet shape) — the OpenSSH
|
//! continue (the ephemeral-fleet shape); the OpenSSH
|
||||||
//! "add to known_hosts?" moment is assembly policy, implemented in
|
//! "add to known_hosts?" moment is assembly policy, implemented in
|
||||||
//! its [`HostKeyPolicy`], never crate behavior.
|
//! its [`HostKeyPolicy`], never crate behavior.
|
||||||
//!
|
//!
|
||||||
//! Crate rules:
|
//! Crate rules:
|
||||||
//!
|
//!
|
||||||
//! - The built-in default policy is **fail-closed**: [`RejectAll`]
|
//! - The built-in default policy is **fail-closed**: [`RejectAll`]
|
||||||
//! rejects every presented key — russh's own reject-all default,
|
//! rejects every presented key, russh's own reject-all default,
|
||||||
//! kept. There is **no accept-all built-in crate option**: a
|
//! kept. There is **no accept-all built-in crate option**: a
|
||||||
//! permissive policy is the assembly's to write when its fleet
|
//! permissive policy is the assembly's to write when its fleet
|
||||||
//! model calls for one.
|
//! model calls for one.
|
||||||
@@ -32,11 +32,11 @@
|
|||||||
//! Result<bool, Self::Error>`): the bool *is* the trust decision,
|
//! Result<bool, Self::Error>`): the bool *is* the trust decision,
|
||||||
//! and the handler's `Err` channel stays wire/IO-failure territory.
|
//! and the handler's `Err` channel stays wire/IO-failure territory.
|
||||||
//! A decline is a disposition, not an error (the crate's
|
//! A decline is a disposition, not an error (the crate's
|
||||||
//! error-model contract) — decline reasons are the policy's own
|
//! error-model contract); decline reasons are the policy's own
|
||||||
//! record, not a typed crate surface.
|
//! record, not a typed crate surface.
|
||||||
//! - The [`PresentedHostKey`]'s fingerprint string is the contract-
|
//! - The [`PresentedHostKey`]'s fingerprint string is the contract-
|
||||||
//! invariant format of the crate's single fingerprint path
|
//! invariant format of the crate's single fingerprint path
|
||||||
//! ([`crate::fingerprint`]) — the wave-4 dial wiring computes it
|
//! ([`crate::fingerprint`]); the wave-4 dial wiring computes it
|
||||||
//! there and surfaces it here.
|
//! there and surfaces it here.
|
||||||
//!
|
//!
|
||||||
//! This module is the seam and its default only: the russh
|
//! This module is the seam and its default only: the russh
|
||||||
@@ -45,17 +45,17 @@
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
/// The presented host key — the decision input to
|
/// The presented host key, the decision input to
|
||||||
/// [`HostKeyPolicy::decide`]: the endpoint the dial contacted and the
|
/// [`HostKeyPolicy::decide`]: the endpoint the dial contacted and the
|
||||||
/// key that endpoint presented.
|
/// key that endpoint presented.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct PresentedHostKey {
|
pub struct PresentedHostKey {
|
||||||
/// The hostname (or IP) the dial targeted — what the assembly's
|
/// The hostname (or IP) the dial targeted, which the assembly's
|
||||||
/// pin store or prompt labels the key against.
|
/// pin store or prompt labels the key against.
|
||||||
pub host: String,
|
pub host: String,
|
||||||
/// The TCP port the dial targeted.
|
/// The TCP port the dial targeted.
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
/// The presented key's fingerprint — the single fingerprint
|
/// The presented key's fingerprint, in the single fingerprint
|
||||||
/// path's `SHA256:` format ([`crate::fingerprint`]); comparison
|
/// path's `SHA256:` format ([`crate::fingerprint`]); comparison
|
||||||
/// against assembly pins or recorded entries is exact string
|
/// against assembly pins or recorded entries is exact string
|
||||||
/// equality by contract.
|
/// equality by contract.
|
||||||
@@ -65,13 +65,13 @@ pub struct PresentedHostKey {
|
|||||||
/// The trust-decision contract the dial surface calls (OQ-05): the
|
/// The trust-decision contract the dial surface calls (OQ-05): the
|
||||||
/// crate surfaces the presented host key (host, port, fingerprint) to
|
/// crate surfaces the presented host key (host, port, fingerprint) to
|
||||||
/// the assembly-provided policy, which decides accept or reject. The
|
/// the assembly-provided policy, which decides accept or reject. The
|
||||||
/// crate is policy-blind — it never decides trust itself and carries
|
/// crate is policy-blind: it never decides trust itself and carries
|
||||||
/// no trust model (see the module docs for the two-case model).
|
/// no trust model (see the module docs for the two-case model).
|
||||||
///
|
///
|
||||||
/// The handle shape is the family's injected-handle pattern
|
/// The handle shape is the family's injected-handle pattern
|
||||||
/// (`Arc<dyn HostKeyPolicy>`, the alkcall `IdentityProvider`
|
/// (`Arc<dyn HostKeyPolicy>`, the alkcall `IdentityProvider`
|
||||||
/// precedent): object-safe via `async_trait`, shared by
|
/// precedent): object-safe via `async_trait`, shared by
|
||||||
/// reference — learn-and-record policies hold their state by interior
|
/// reference; learn-and-record policies hold their state by interior
|
||||||
/// mutability so the same handle serves the dial and any inspection.
|
/// mutability so the same handle serves the dial and any inspection.
|
||||||
///
|
///
|
||||||
/// **Fail-closed posture**: when a policy cannot decide (its pin
|
/// **Fail-closed posture**: when a policy cannot decide (its pin
|
||||||
@@ -81,19 +81,19 @@ pub struct PresentedHostKey {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait HostKeyPolicy: Send + Sync + 'static {
|
pub trait HostKeyPolicy: Send + Sync + 'static {
|
||||||
/// Decides the trust verdict for one presented host key: `true`
|
/// Decides the trust verdict for one presented host key: `true`
|
||||||
/// accepts the key for this dial, `false` declines it — the dial
|
/// accepts the key for this dial, `false` declines it; the dial
|
||||||
/// connection aborts on decline (russh's `check_server_key`
|
/// connection aborts on decline (russh's `check_server_key`
|
||||||
/// `false` → `Error::UnknownKey`, a hard, clean decline; a
|
/// `false` → `Error::UnknownKey`, a hard, clean decline; a
|
||||||
/// mismatched pin is a hard decline, never a soft failure).
|
/// mismatched pin is a hard decline, never a soft failure).
|
||||||
async fn decide(&self, presented: &PresentedHostKey) -> bool;
|
async fn decide(&self, presented: &PresentedHostKey) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The built-in default host-key policy — **fail-closed**: reject
|
/// The built-in default host-key policy, **fail-closed**: reject
|
||||||
/// every presented key.
|
/// every presented key.
|
||||||
///
|
///
|
||||||
/// russh's own reject-all default, kept as the crate's built-in
|
/// russh's own reject-all default, kept as the crate's built-in
|
||||||
/// default policy (`docs/architecture/auth.md` §Host-key policy). A
|
/// default policy (`docs/architecture/auth.md` §Host-key policy). A
|
||||||
/// dial that ships this policy can never connect — by design: trust
|
/// dial that ships this policy can never connect, by design: trust
|
||||||
/// is the assembly's decision to make, and the crate's default must
|
/// is the assembly's decision to make, and the crate's default must
|
||||||
/// never let a dial through on the crate's authority. There is no
|
/// never let a dial through on the crate's authority. There is no
|
||||||
/// accept-all counterpart in the crate; the permissive case-2 policy
|
/// accept-all counterpart in the crate; the permissive case-2 policy
|
||||||
|
|||||||
+47
-46
@@ -1,4 +1,4 @@
|
|||||||
//! The vault-backed key sources — ADR-005's custody model in code,
|
//! The vault-backed key sources: ADR-005's custody model in code,
|
||||||
//! both directions (client dial keys, server host keys).
|
//! both directions (client dial keys, server host keys).
|
||||||
//!
|
//!
|
||||||
//! The adapter takes *key sources*, not static keys (the alkhttp
|
//! The adapter takes *key sources*, not static keys (the alkhttp
|
||||||
@@ -7,26 +7,24 @@
|
|||||||
//! implements them over an injected alkvault handle. The crate ships
|
//! implements them over an injected alkvault handle. The crate ships
|
||||||
//! the vault-backed implementations for the ADR-005 shape
|
//! the vault-backed implementations for the ADR-005 shape
|
||||||
//! ([`VaultKeySource`] for one dial key, [`VaultHostKeySource`] for
|
//! ([`VaultKeySource`] for one dial key, [`VaultHostKeySource`] for
|
||||||
//! the host-key set) along with the decrypt-at-use chain they run —
|
//! the host-key set) along with the decrypt-at-use chain they run:
|
||||||
//! the custody machinery is the crate's, the at-rest storage is the
|
//! the custody machinery is the crate's, the at-rest storage is the
|
||||||
//! assembly's.
|
//! assembly's.
|
||||||
//!
|
//!
|
||||||
//! Crate rules, per ADR-005:
|
//! Crate rules, per ADR-005:
|
||||||
//!
|
//!
|
||||||
//! - **`EncryptedData` is opaque** (decision 5): blobs flow through
|
//! - **`EncryptedData` is opaque** (decision 5): blobs flow through
|
||||||
//! this module to `VaultServiceHandle::decrypt` untouched — no
|
//! this module to `VaultServiceHandle::decrypt` untouched: no
|
||||||
//! `key_version` interpretation, no record-internals access
|
//! `key_version` interpretation, no record-internals access
|
||||||
//! anywhere in the crate; version semantics belong to the vault.
|
//! anywhere in the crate; version semantics belong to the vault.
|
||||||
//! - **At-use lifecycle** (decision 2): `vault.decrypt(blob)` →
|
//! - **At-use lifecycle** (decision 2): `vault.decrypt(blob)` →
|
||||||
//! `Zeroizing<String>` wrap at the adapter seam (decision 3 — the
|
//! the at-use chain's single-use read → one-time
|
||||||
//! known-gap contract; the vault-side improvement is upstream, not
|
|
||||||
//! here) → one-time
|
|
||||||
//! [`decode_secret_key`](russh::keys::decode_secret_key) → the
|
//! [`decode_secret_key`](russh::keys::decode_secret_key) → the
|
||||||
//! decoded `PrivateKey` lives for the dial's lifetime. The vault is
|
//! decoded `PrivateKey` lives for the dial's lifetime. The vault is
|
||||||
//! needed only at use time: locking the vault mid-session keeps
|
//! needed only at use time: locking the vault mid-session keeps
|
||||||
//! live sessions streaming while new loads fail (POC-2 §2).
|
//! live sessions streaming while new loads fail (POC-2 §2).
|
||||||
//! - **Inner format is OpenSSH private-key PEM, unencrypted**
|
//! - **Inner format is OpenSSH private-key PEM, unencrypted**
|
||||||
//! (decision 1) — the source contract assumes it; the decode call
|
//! (decision 1), which the source contract assumes; the decode call
|
||||||
//! is the single conversion. A plaintext that is not that shape is
|
//! is the single conversion. A plaintext that is not that shape is
|
||||||
//! a [`KeySourceError::Decode`] refusal, not a fallback.
|
//! a [`KeySourceError::Decode`] refusal, not a fallback.
|
||||||
//! - **Failure modes are fail-closed with distinct variants**
|
//! - **Failure modes are fail-closed with distinct variants**
|
||||||
@@ -37,7 +35,7 @@
|
|||||||
//! - **Plaintext never persists** (decision 7): no disk spill on this
|
//! - **Plaintext never persists** (decision 7): no disk spill on this
|
||||||
//! path, ever.
|
//! path, ever.
|
||||||
//! - Vault lifecycle is the assembly's: this module never unlocks
|
//! - Vault lifecycle is the assembly's: this module never unlocks
|
||||||
//! the vault and never retries — a locked vault stays a refusal.
|
//! the vault and never retries; a locked vault stays a refusal.
|
||||||
|
|
||||||
use alkvault::{EncryptedData, VaultServiceError, VaultServiceHandle};
|
use alkvault::{EncryptedData, VaultServiceError, VaultServiceHandle};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -46,7 +44,7 @@ use zeroize::Zeroizing;
|
|||||||
|
|
||||||
use crate::error::KeySourceError;
|
use crate::error::KeySourceError;
|
||||||
|
|
||||||
/// The client dial-key source seam — the handle the wave-4 dial-config
|
/// The client dial-key source seam, the handle the wave-4 dial-config
|
||||||
/// carries (never a static key, never env material).
|
/// carries (never a static key, never env material).
|
||||||
///
|
///
|
||||||
/// Produces the decoded [`PrivateKey`] for one dial, decrypt-at-use:
|
/// Produces the decoded [`PrivateKey`] for one dial, decrypt-at-use:
|
||||||
@@ -54,12 +52,12 @@ use crate::error::KeySourceError;
|
|||||||
/// key outlives any vault state change (locking mid-session keeps
|
/// key outlives any vault state change (locking mid-session keeps
|
||||||
/// established sessions streaming; POC-2 §2). Implementations must be
|
/// established sessions streaming; POC-2 §2). Implementations must be
|
||||||
/// fail-closed: a load that cannot produce key material returns the
|
/// fail-closed: a load that cannot produce key material returns the
|
||||||
/// matching [`KeySourceError`] variant — every variant is a
|
/// matching [`KeySourceError`] variant; every variant is a
|
||||||
/// no-dial outcome, and there is no fallback key shape in the crate.
|
/// no-dial outcome, and there is no fallback key shape in the crate.
|
||||||
///
|
///
|
||||||
/// The handle shape is the family's injected-handle pattern
|
/// The handle shape is the family's injected-handle pattern
|
||||||
/// (`Arc<dyn KeySource>`, the alkcall `IdentityProvider` precedent):
|
/// (`Arc<dyn KeySource>`, the alkcall `IdentityProvider` precedent):
|
||||||
/// object-safe via `async_trait`, shared by reference — the vault
|
/// object-safe via `async_trait`, shared by reference; the vault
|
||||||
/// handle rides the implementation (the assembly clones its
|
/// handle rides the implementation (the assembly clones its
|
||||||
/// [`VaultServiceHandle`] into its source). The crate's ADR-005
|
/// [`VaultServiceHandle`] into its source). The crate's ADR-005
|
||||||
/// implementation is [`VaultKeySource`]; an assembly may implement the
|
/// implementation is [`VaultKeySource`]; an assembly may implement the
|
||||||
@@ -67,48 +65,48 @@ use crate::error::KeySourceError;
|
|||||||
/// contract (OpenSSH-PEM, unencrypted).
|
/// contract (OpenSSH-PEM, unencrypted).
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait KeySource: Send + Sync + 'static {
|
pub trait KeySource: Send + Sync + 'static {
|
||||||
/// Loads the dial key — decryption plus the one-time
|
/// Loads the dial key: decryption plus the one-time
|
||||||
/// `decode_secret_key` (ADR-005 decision 2).
|
/// `decode_secret_key` (ADR-005 decision 2).
|
||||||
///
|
///
|
||||||
/// This is the only surface where the dial direction touches
|
/// This is the only surface where the dial direction touches
|
||||||
/// secret material: the plaintext exists solely inside this
|
/// secret material: the plaintext exists solely inside this
|
||||||
/// method and is dropped (zeroized from the adapter seam onward)
|
/// method and is zeroized by the vault's `Zeroizing<String>` return
|
||||||
/// before the decoded key is returned.
|
/// at its drop, before the decoded key is returned.
|
||||||
async fn load(&self) -> Result<PrivateKey, KeySourceError>;
|
async fn load(&self) -> Result<PrivateKey, KeySourceError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The server host-key source seam — the handle the wave-2
|
/// The server host-key source seam, the handle the wave-2
|
||||||
/// serve-config carries (ADR-005 decision 6).
|
/// serve-config carries (ADR-005 decision 6).
|
||||||
///
|
///
|
||||||
/// Produces the decoded host-key set for server construction —
|
/// Produces the decoded host-key set for server construction,
|
||||||
/// decrypt-**at-construction**: the wave-2 `Config` builder calls
|
/// decrypt-**at-construction**: the wave-2 `Config` builder calls
|
||||||
/// [`HostKeySource::load`] when it assembles the server, and a failure
|
/// [`HostKeySource::load`] when it assembles the server, and a failure
|
||||||
/// means no server is constructed (POC-2 §5; locked vault at
|
/// means no server is constructed (POC-2 §5; locked vault at
|
||||||
/// bootstrap → [`KeySourceError::Locked`] → fail-closed, never a
|
/// bootstrap → [`KeySourceError::Locked`] → fail-closed, never a
|
||||||
/// half-configured server). The crate is policy-blind on host *pin*
|
/// half-configured server). The crate is policy-blind on host *pin*
|
||||||
/// trust — that is the [`crate::HostKeyPolicy`] seam's business — this
|
/// trust (that is the [`crate::HostKeyPolicy`] seam's business); this
|
||||||
/// seam is only the *held* material (the client's host pin is public
|
/// seam is only the *held* material (the client's host pin is public
|
||||||
/// config, never vault material).
|
/// config, never vault material).
|
||||||
///
|
///
|
||||||
/// The set, not a single key: `Config::keys` accepts multiple
|
/// The set, not a single key: `Config::keys` accepts multiple
|
||||||
/// host keys and fleets rotate them. Implementations must be
|
/// host keys and fleets rotate them. Implementations must be
|
||||||
/// fail-closed in the same shape as [`KeySource`] — see the trait's
|
/// fail-closed in the same shape as [`KeySource`]; see the trait's
|
||||||
/// docs. The crate's ADR-005 implementation is [`VaultHostKeySource`].
|
/// docs. The crate's ADR-005 implementation is [`VaultHostKeySource`].
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait HostKeySource: Send + Sync + 'static {
|
pub trait HostKeySource: Send + Sync + 'static {
|
||||||
/// Loads the host-key set — each blob through the same
|
/// Loads the host-key set, each blob through the same
|
||||||
/// decrypt-at-use chain as [`KeySource::load`]. Order follows the
|
/// decrypt-at-use chain as [`KeySource::load`]. Order follows the
|
||||||
/// source's registration order.
|
/// source's registration order.
|
||||||
async fn load(&self) -> Result<Vec<PrivateKey>, KeySourceError>;
|
async fn load(&self) -> Result<Vec<PrivateKey>, KeySourceError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ADR-005 dial-key source — decrypt-at-use over an injected
|
/// The ADR-005 dial-key source, decrypt-at-use over an injected
|
||||||
/// alkvault handle, backed by one at-rest `EncryptedData` record.
|
/// alkvault handle, backed by one at-rest `EncryptedData` record.
|
||||||
///
|
///
|
||||||
/// The record is injected at construction, from the assembly's
|
/// The record is injected at construction, from the assembly's
|
||||||
/// registry (`None` = the assembly holds no record at this key's
|
/// registry (`None` = the assembly holds no record at this key's
|
||||||
/// location → [`KeySourceError::Absent`], POC-2 §3). The vault handle
|
/// location → [`KeySourceError::Absent`], POC-2 §3). The vault handle
|
||||||
/// is a clone of the assembly's — this source never unlocks, locks, or
|
/// is a clone of the assembly's; this source never unlocks, locks, or
|
||||||
/// retries the vault's lifecycle state; every [`VaultKeySource::load`]
|
/// retries the vault's lifecycle state; every [`VaultKeySource::load`]
|
||||||
/// is decrypt-at-usage and a locked vault is a clean
|
/// is decrypt-at-usage and a locked vault is a clean
|
||||||
/// [`KeySourceError::Locked`] refusal.
|
/// [`KeySourceError::Locked`] refusal.
|
||||||
@@ -136,11 +134,11 @@ impl KeySource for VaultKeySource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ADR-005 host-key source — decrypt-at-construction over an
|
/// The ADR-005 host-key source, decrypt-at-construction over an
|
||||||
/// injected alkvault handle, backed by the injected at-rest record set.
|
/// injected alkvault handle, backed by the injected at-rest record set.
|
||||||
///
|
///
|
||||||
/// Each element of the set flows through the same decrypt-and-decode
|
/// Each element of the set flows through the same decrypt-and-decode
|
||||||
/// chain as [`VaultKeySource`] (one model, both directions —
|
/// chain as [`VaultKeySource`] (one model, both directions;
|
||||||
/// POC-2 §5). An empty set is [`KeySourceError::Absent`] (a host key
|
/// POC-2 §5). An empty set is [`KeySourceError::Absent`] (a host key
|
||||||
/// registration gap), which the wave-2 config builder treats exactly
|
/// registration gap), which the wave-2 config builder treats exactly
|
||||||
/// like every other load failure: no server is constructed.
|
/// like every other load failure: no server is constructed.
|
||||||
@@ -171,45 +169,48 @@ impl HostKeySource for VaultHostKeySource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ADR-005 at-use chain (decision 2): one vault decrypt, the
|
/// The ADR-005 at-use chain (decision 2): one vault decrypt, one
|
||||||
/// `Zeroizing<String>` seam wrap, one `decode_secret_key`.
|
/// `decode_secret_key`.
|
||||||
///
|
///
|
||||||
/// The plaintext `String` leaves the vault as
|
/// The plaintext leaves the vault as
|
||||||
/// [`KeySourceError`]-mapped failure or enters the wrap immediately —
|
/// [`KeySourceError`]-mapped failure or as alkvault 0.2's
|
||||||
/// its only unfenced window is the vault's own `String` return, the
|
/// `Zeroizing<String>` return (the pre-wrap unfenced window ADR-005
|
||||||
/// known-gap contract of ADR-005 decision 3 (the upstream ask is
|
/// decision 3 recorded is closed upstream: the vault fences the
|
||||||
/// alkvault's; OQ-07). The decode is the single inner-format
|
/// plaintext from its return onward, zeroized on drop, including the
|
||||||
/// conversion (decision 1); a plaintext that is not OpenSSH-PEM key
|
/// vault's own UTF-8 failure path; OQ-07's upstream ask is resolved,
|
||||||
/// material is a [`KeySourceError::Decode`] refusal.
|
/// amending the ADR's decision 3 note). The decode is the single
|
||||||
|
/// inner-format conversion (decision 1); a plaintext that is not
|
||||||
|
/// OpenSSH-PEM key material is a [`KeySourceError::Decode`] refusal.
|
||||||
fn decrypt_and_decode(
|
fn decrypt_and_decode(
|
||||||
vault: &VaultServiceHandle,
|
vault: &VaultServiceHandle,
|
||||||
blob: &EncryptedData,
|
blob: &EncryptedData,
|
||||||
) -> Result<PrivateKey, KeySourceError> {
|
) -> Result<PrivateKey, KeySourceError> {
|
||||||
let plaintext = Zeroizing::new(vault.decrypt(blob).map_err(map_vault_error)?);
|
let plaintext = vault.decrypt(blob).map_err(map_vault_error)?;
|
||||||
decode_key_arm(&plaintext)
|
decode_key_arm(&plaintext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The at-use chain's decode arm — the one-time
|
/// The at-use chain's decode arm: the one-time
|
||||||
/// [`decode_secret_key`] conversion of [`decrypt_and_decode`]
|
/// [`decode_secret_key`] conversion of [`decrypt_and_decode`]
|
||||||
/// (ADR-005 decision 2's single conversion; this function is byte-for-
|
/// (ADR-005 decision 2's single conversion; this function is byte-for-
|
||||||
/// byte that arm, extracted so the arm is the *only* shape the fuzz
|
/// byte that arm, extracted so the arm is the *only* shape the fuzz
|
||||||
/// target drives). `#[doc(hidden)]` fuzz-visible surface
|
/// target drives). `#[doc(hidden)]` fuzz-visible surface
|
||||||
/// (`w5-fuzz-harness`; the recorded exposure decision — the ingress is
|
/// (`w5-fuzz-harness`; the recorded exposure decision: the ingress is
|
||||||
/// production-private, so the exposure is the production arm itself,
|
/// production-private, so the exposure is the production arm itself,
|
||||||
/// not a fork). Not documented API: the vault-facing surface is
|
/// not a fork). Not documented API: the vault-facing surface is
|
||||||
/// exactly [`VaultKeySource`] / [`VaultHostKeySource`].
|
/// exactly [`VaultKeySource`] / [`VaultHostKeySource`].
|
||||||
///
|
///
|
||||||
/// The `Zeroizing` posture rides the signature: the plaintext arrives
|
/// The `Zeroizing` posture rides the signature: the plaintext
|
||||||
/// already wrapped at the adapter seam (decision 3), the decode reads
|
/// arrives already zeroized (alkvault 0.2's return type, decision
|
||||||
/// it once, and nothing is retained — `Ok` carries the decoded
|
/// 3's ownership moved upstream), the decode reads
|
||||||
|
/// it once, and nothing is retained: `Ok` carries the decoded
|
||||||
/// [`PrivateKey`], `Err` carries only the typed [`KeySourceError`]
|
/// [`PrivateKey`], `Err` carries only the typed [`KeySourceError`]
|
||||||
/// class (never the plaintext).
|
/// class (never the plaintext).
|
||||||
///
|
///
|
||||||
/// The typed `Decode` payload is crate-owned fixed text, deliberately
|
/// The typed `Decode` payload is crate-owned fixed text, deliberately
|
||||||
/// *not* the underlying machinery's message (the `w5-fuzz-harness`
|
/// *not* the underlying machinery's message (the `w5-fuzz-harness`
|
||||||
/// finding, 2026-10-30): russh's/`ssh-key`'s PPK parse path
|
/// finding, 2026-10-30): russh's/`ssh-key`'s PPK parse path
|
||||||
/// (`PpkParseError::Header`) interpolates raw header bytes — the
|
/// (`PpkParseError::Header`) interpolates raw header bytes, the
|
||||||
/// plaintext's own material — into its Display, so a machinery message
|
/// plaintext's own material, into its Display, so a machinery message
|
||||||
/// riding the typed payload would echo decrypted-record material
|
/// riding the typed payload would echo decrypted-record material
|
||||||
/// exactly the no-persist posture (decision 7) forbids. The upstream
|
/// exactly the no-persist posture (decision 7) forbids. The upstream
|
||||||
/// echo ask is recorded with the task; if the machinery gains an
|
/// echo ask is recorded with the task; if the machinery gains an
|
||||||
@@ -218,7 +219,7 @@ fn decrypt_and_decode(
|
|||||||
pub fn decode_key_arm(plaintext: &Zeroizing<String>) -> Result<PrivateKey, KeySourceError> {
|
pub fn decode_key_arm(plaintext: &Zeroizing<String>) -> Result<PrivateKey, KeySourceError> {
|
||||||
decode_secret_key(plaintext, None).map_err(|_| {
|
decode_secret_key(plaintext, None).map_err(|_| {
|
||||||
KeySourceError::Decode(
|
KeySourceError::Decode(
|
||||||
"the plaintext is not OpenSSH private-key PEM material — the decode machinery \
|
"the plaintext is not OpenSSH private-key PEM material; the decode machinery \
|
||||||
refused it (the machinery's message is not carried: it can echo plaintext \
|
refused it (the machinery's message is not carried: it can echo plaintext \
|
||||||
material; the upstream echo ask is recorded)"
|
material; the upstream echo ask is recorded)"
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
@@ -227,18 +228,18 @@ pub fn decode_key_arm(plaintext: &Zeroizing<String>) -> Result<PrivateKey, KeySo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The vault's error taxonomy reshaped into ADR-005 decision 4's
|
/// The vault's error taxonomy reshaped into ADR-005 decision 4's
|
||||||
/// five-variant matrix — never re-exported, never collapsed into one.
|
/// five-variant matrix, never re-exported, never collapsed into one.
|
||||||
///
|
///
|
||||||
/// `VaultLocked` → [`KeySourceError::Locked`]; `InvalidPath` (the
|
/// `VaultLocked` → [`KeySourceError::Locked`]; `InvalidPath` (the
|
||||||
/// vault's refuse-before-crypto refusal of an unresolvable derivation
|
/// vault's refuse-before-crypto refusal of an unresolvable derivation
|
||||||
/// path — reserved/legacy v1, v0) →
|
/// path, such as reserved/legacy v1 or v0) →
|
||||||
/// [`KeySourceError::UnknownKeyVersion`]; `Encryption` (the GCM-open
|
/// [`KeySourceError::UnknownKeyVersion`]; `Encryption` (the GCM-open
|
||||||
/// class: wrong mnemonic, tampered ciphertext) is honest refuted
|
/// class: wrong mnemonic, tampered ciphertext) is honest refuted
|
||||||
/// decryption → [`KeySourceError::Decryption`]. The remaining vault
|
/// decryption → [`KeySourceError::Decryption`]. The remaining vault
|
||||||
/// variants cannot arise on the decrypt path (decryption never
|
/// variants cannot arise on the decrypt path (decryption never
|
||||||
/// touches mnemonic parsing, key derivation outside version-path
|
/// touches mnemonic parsing, key derivation outside version-path
|
||||||
/// resolution, unlock state transitions, or key-type registration;
|
/// resolution, unlock state transitions, or key-type registration;
|
||||||
/// POC-2 §3) — if one were ever observed it would be a vault-side
|
/// POC-2 §3); if one were ever observed it would be a vault-side
|
||||||
/// refuse-to-decrypt condition, so it lands in the `Decryption` class
|
/// refuse-to-decrypt condition, so it lands in the `Decryption` class
|
||||||
/// with the vault's message intact rather than silently succeeding.
|
/// with the vault's message intact rather than silently succeeding.
|
||||||
fn map_vault_error(error: VaultServiceError) -> KeySourceError {
|
fn map_vault_error(error: VaultServiceError) -> KeySourceError {
|
||||||
@@ -272,14 +273,14 @@ mod tests {
|
|||||||
use crate::error::KeySourceError;
|
use crate::error::KeySourceError;
|
||||||
use crate::fingerprint::fingerprint;
|
use crate::fingerprint::fingerprint;
|
||||||
|
|
||||||
/// The committed test-key fixtures (`tests/vectors/`) — the
|
/// The committed test-key fixtures (`tests/vectors/`), the
|
||||||
/// wave-1 fixture set, read by this `#[cfg(test)]` module through
|
/// wave-1 fixture set, read by this `#[cfg(test)]` module through
|
||||||
/// `include_str!` (the `tests/common/mod.rs` double module is the
|
/// `include_str!` (the `tests/common/mod.rs` double module is the
|
||||||
/// tests/-side owner; src-unit tests never own fixture copies).
|
/// tests/-side owner; src-unit tests never own fixture copies).
|
||||||
/// Throwaway `ssh-keygen`-minted keys, never registered anywhere
|
/// Throwaway `ssh-keygen`-minted keys, never registered anywhere
|
||||||
/// real (`ALPHA` is the client-dial fixture; `BRAVO`/`CHARLIE`
|
/// real (`ALPHA` is the client-dial fixture; `BRAVO`/`CHARLIE`
|
||||||
/// the host-key pair). Their `ssh-keygen -lf` fingerprints are
|
/// the host-key pair). Their `ssh-keygen -lf` fingerprints are
|
||||||
/// recorded in `tests/vectors/fingerprints.txt` — the fixture
|
/// recorded in `tests/vectors/fingerprints.txt`; the fixture
|
||||||
/// parity row below asserts this module sees the same values.
|
/// parity row below asserts this module sees the same values.
|
||||||
const ALPHA_PEM: &str = include_str!("../tests/vectors/alpha.key");
|
const ALPHA_PEM: &str = include_str!("../tests/vectors/alpha.key");
|
||||||
const BRAVO_PEM: &str = include_str!("../tests/vectors/bravo.key");
|
const BRAVO_PEM: &str = include_str!("../tests/vectors/bravo.key");
|
||||||
|
|||||||
+33
-33
@@ -1,61 +1,61 @@
|
|||||||
//! alkssh — the alk family's SSH adapter (the alkhttp-style door for the
|
//! alkssh, the alk family's SSH adapter (the alkhttp-style door for the
|
||||||
//! SSH protocol): a thin wrapper around russh terminating the vanilla-ssh
|
//! SSH protocol): a thin wrapper around russh terminating the vanilla-ssh
|
||||||
//! wire and bridging to/from alkcall channels (the `alk/ssh` ALPN).
|
//! wire and bridging to/from alkcall channels (the `alk/ssh` ALPN).
|
||||||
//!
|
//!
|
||||||
//! No SSH framing is authored in-family (family ADR-003): russh owns the
|
//! No SSH framing is authored in-family (family ADR-003): russh owns the
|
||||||
//! wire, and the crate's substance is the channel-type mappings —
|
//! wire, and the crate's substance is the channel-type mappings:
|
||||||
//! session/pty → alktty, direct/forwarded-tcpip → alktunnels, git exec →
|
//! session/pty → alktty, direct/forwarded-tcpip → alktunnels, git exec →
|
||||||
//! alkgit, sftp → alkfs later (a declining placeholder until then), each
|
//! alkgit, sftp → alkfs later (a declining placeholder until then), each
|
||||||
//! behind its cargo feature. Git over SSH is exec-only (ADR-006): a
|
//! behind its cargo feature. Git over SSH is exec-only (ADR-006): a
|
||||||
//! subsystem request can never carry the repository, so it declines. A
|
//! subsystem request can never carry the repository, so it declines. A
|
||||||
//! client opening an unfeatured or unimplemented channel type gets a
|
//! client opening an unfeatured or unimplemented channel type gets a
|
||||||
//! clean decline — never a half-capability, never a silent fallback; the
|
//! clean decline, never a half-capability, never a silent fallback; the
|
||||||
//! default build (no features) is structurally incapable of serving
|
//! default build (no features) is structurally incapable of serving
|
||||||
//! anything but declines.
|
//! anything but declines.
|
||||||
//!
|
//!
|
||||||
//! Identity is the family's, not SSH's: a verified key's fingerprint →
|
//! Identity is the family's, not SSH's: a verified key's fingerprint →
|
||||||
//! alkcall `IdentityProvider`. Keys the crate holds (client dial keys,
|
//! alkcall `IdentityProvider`. Keys the crate holds (client dial keys,
|
||||||
//! host keys) come from alkvault-backed sources — vault-encrypted at
|
//! host keys) come from alkvault-backed sources, vault-encrypted at
|
||||||
//! rest, decrypted at use (ADR-005), never env vars.
|
//! rest, decrypted at use (ADR-005), never env vars.
|
||||||
//!
|
//!
|
||||||
//! Architecture: `docs/architecture/` (phase 1 — ADRs 001–006 accepted;
|
//! Architecture: `docs/architecture/` (phase 1: ADRs 001–006 accepted;
|
||||||
//! the foundations, the server stack, the server-direction mappings, and
|
//! the foundations, the server stack, the server-direction mappings, and
|
||||||
//! the client direction — dial, op rows, the `-R` acceptor leg, and the
|
//! the client direction (dial, op rows, the `-R` acceptor leg, and the
|
||||||
//! lifecycle primitives — are landed through wave 4; the validation
|
//! lifecycle primitives) are landed through wave 4; the validation
|
||||||
//! suite and the release are wave 5). See also `docs/research/`,
|
//! suite and the release are wave 5). See also `docs/research/`,
|
||||||
//! `docs/plans/implementation.md`, and family ADR-003.
|
//! `docs/plans/implementation.md`, and family ADR-003.
|
||||||
//!
|
//!
|
||||||
//! # The feature graph (ADR-003 — flat, independent, fail-closed)
|
//! # The feature graph (ADR-003: flat, independent, fail-closed)
|
||||||
//!
|
//!
|
||||||
//! Every channel-type mapping is a cargo feature; a session opening an
|
//! Every channel-type mapping is a cargo feature; a session opening an
|
||||||
//! unfeatured or unimplemented channel type is declined fail-closed —
|
//! unfeatured or unimplemented channel type is declined fail-closed,
|
||||||
//! never a half-capability, never a silent fallback. **A default build
|
//! never a half-capability, never a silent fallback. **A default build
|
||||||
//! (no features) has no capabilities**: it can serve nothing but clean
|
//! (no features) has no capabilities**: it can serve nothing but clean
|
||||||
//! declines. No feature implies another; the graph is flat.
|
//! declines. No feature implies another; the graph is flat.
|
||||||
//!
|
//!
|
||||||
//! - (default, no features) — serves declines only: every open, exec,
|
//! - (default, no features) serves declines only: every open, exec,
|
||||||
//! listen request answers a clean, channel-scoped refusal.
|
//! listen request answers a clean, channel-scoped refusal.
|
||||||
//! - `session` — pty/shell/exec rows → the `RemoteSession` chunk
|
//! - `session` maps the pty/shell/exec rows → the `RemoteSession` chunk
|
||||||
//! plane (alktty).
|
//! plane (alktty).
|
||||||
//! - `git` — the exec-only git rows (`git-{upload,receive}-pack`) →
|
//! - `git` carries the exec-only git rows (`git-{upload,receive}-pack`) →
|
||||||
//! alkgit; a git *subsystem* request always declines (the repository
|
//! alkgit; a git *subsystem* request always declines (the repository
|
||||||
//! can never ride a subsystem channel).
|
//! can never ride a subsystem channel).
|
||||||
//! - `tunnel` — `direct-tcpip` / `direct-streamlocal` and the `-R`
|
//! - `tunnel` covers `direct-tcpip` / `direct-streamlocal` and the `-R`
|
||||||
//! legs (listen, `forwarded-tcpip` / `forwarded-streamlocal`) →
|
//! legs (listen, `forwarded-tcpip` / `forwarded-streamlocal`) →
|
||||||
//! alktunnels.
|
//! alktunnels.
|
||||||
//! - `sftp` — a reserved, currently-empty placeholder (OQ-13): the
|
//! - `sftp`, a reserved, currently-empty placeholder (OQ-13): the
|
||||||
//! `sftp` subsystem request still declines in every build until the
|
//! `sftp` subsystem request still declines in every build until the
|
||||||
//! alkfs mapping lands; the feature exists so the decline stays
|
//! alkfs mapping lands; the feature exists so the decline stays
|
||||||
//! contract territory (tested in every matrix build).
|
//! contract territory (tested in every matrix build).
|
||||||
//!
|
//!
|
||||||
//! Advertisement is honest: an unfeatured channel type is never
|
//! Advertisement is honest: an unfeatured channel type is never
|
||||||
//! advertised, only declined (`docs/architecture/mapping.md` §Decline
|
//! advertised, only declined (`docs/architecture/mapping.md` §Decline
|
||||||
//! matrix — the fail-closed invariant is contract territory on both
|
//! matrix; the fail-closed invariant is contract territory on both
|
||||||
//! sides of every feature).
|
//! sides of every feature).
|
||||||
//!
|
//!
|
||||||
//! # Known limits (recorded dispositions riding the published docs)
|
//! # Known limits (recorded dispositions riding the published docs)
|
||||||
//!
|
//!
|
||||||
//! The records below are deliberate dispositions — documented limits
|
//! The records below are deliberate dispositions: documented limits
|
||||||
//! with their reasons, mirrored in the module docs and the
|
//! with their reasons, mirrored in the module docs and the
|
||||||
//! architecture spec, not defects awaiting a fix:
|
//! architecture spec, not defects awaiting a fix:
|
||||||
//!
|
//!
|
||||||
@@ -64,29 +64,29 @@
|
|||||||
//! per-channel window, the client's own flow control) and liveness
|
//! per-channel window, the client's own flow control) and liveness
|
||||||
//! is covered by the always-set
|
//! is covered by the always-set
|
||||||
//! [`Limits::inactivity_timeout`]; a deadline here would be a
|
//! [`Limits::inactivity_timeout`]; a deadline here would be a
|
||||||
//! behavior change (a slow client may stall its stream — vanilla
|
//! behavior change (a slow client may stall its stream, vanilla
|
||||||
//! sshd's posture included). (`tty_map.rs` module docs.)
|
//! sshd's posture included). (`tty_map.rs` module docs.)
|
||||||
//! - **The PTY path's exit-signal fidelity is limited by upstream**
|
//! - **The PTY path's exit-signal fidelity is limited by upstream**
|
||||||
//! (exit-currency rule 5): signal-terminated PTY sessions surface as
|
//! (exit-currency rule 5): signal-terminated PTY sessions surface as
|
||||||
//! `exit-status(1)` because the backend's reap surface loses the
|
//! `exit-status(1)` because the backend's reap surface loses the
|
||||||
//! signal number; the pipe path encodes negatives faithfully. An
|
//! signal number; the pipe path encodes negatives faithfully. An
|
||||||
//! alktty/portable-pty ask — the contract adapts automatically
|
//! alktty/portable-pty ask; the contract adapts automatically
|
||||||
//! when upstream conforms.
|
//! when upstream conforms.
|
||||||
//! - **The port-0 mint is the only bound-port echo the wire carries**
|
//! - **The port-0 mint is the only bound-port echo the wire carries**
|
||||||
//! (russh/sshd parity): a `tcpip_forward` success reply carries the
|
//! (russh/sshd parity): a `tcpip_forward` success reply carries the
|
||||||
//! allocated port only for a `port = 0` request; an explicit-port
|
//! allocated port only for a `port = 0` request; an explicit-port
|
||||||
//! request's bound port is by definition the requested one — the
|
//! request's bound port is by definition the requested one; the
|
||||||
//! crate surfaces it as such, never a fabricated number.
|
//! crate surfaces it as such, never a fabricated number.
|
||||||
//! - **No reconnect/restore policy.** The client direction ships
|
//! - **No reconnect/restore policy.** The client direction ships
|
||||||
//! lifecycle *primitives* (bound ports, cancellable handles, clean
|
//! lifecycle *primitives* (bound ports, cancellable handles, clean
|
||||||
//! [`SshConnection::close`]); reconnect-and-restore (rebinding
|
//! [`SshConnection::close`]); reconnect-and-restore (rebinding
|
||||||
//! persisted tunnel ports) is assembly composition over them — the
|
//! persisted tunnel ports) is assembly composition over them; the
|
||||||
//! crate never owns the policy. Drop is never the clean path at
|
//! crate never owns the policy. Drop is never the clean path at
|
||||||
//! this russh pin: only an explicit `close` sends the clean
|
//! this russh pin: only an explicit `close` sends the clean
|
||||||
//! disconnect.
|
//! disconnect.
|
||||||
//! - **No accept-all host-key policy exists in the crate.** The
|
//! - **No accept-all host-key policy exists in the crate.** The
|
||||||
//! built-in default is [`RejectAll`] (a dial with it can never
|
//! built-in default is [`RejectAll`] (a dial with it can never
|
||||||
//! connect — by design); the crate is policy-blind and any
|
//! connect, by design); the crate is policy-blind and any
|
||||||
//! permissive trust policy is the assembly's to write. A
|
//! permissive trust policy is the assembly's to write. A
|
||||||
//! fail-closed policy posture: when a policy cannot decide, it must
|
//! fail-closed policy posture: when a policy cannot decide, it must
|
||||||
//! decline.
|
//! decline.
|
||||||
@@ -96,10 +96,10 @@
|
|||||||
//! The crate is a door, not a deployment: an assembly embeds it,
|
//! The crate is a door, not a deployment: an assembly embeds it,
|
||||||
//! enables the features it wants, and injects the seams (identity,
|
//! enables the features it wants, and injects the seams (identity,
|
||||||
//! key sources, the per-feature family handles). The examples are
|
//! key sources, the per-feature family handles). The examples are
|
||||||
//! compile-checked (`no_run`) doctests — honest sketches, not
|
//! compile-checked (`no_run`) doctests, honest sketches, not
|
||||||
//! executable code.
|
//! executable code.
|
||||||
//!
|
//!
|
||||||
//! A server mount (the default build — a decline-only door; the
|
//! A server mount (the default build, a decline-only door; the
|
||||||
//! assembly's real sources are the vault-backed [`VaultKeySource`] /
|
//! assembly's real sources are the vault-backed [`VaultKeySource`] /
|
||||||
//! [`VaultHostKeySource`] shapes):
|
//! [`VaultHostKeySource`] shapes):
|
||||||
//!
|
//!
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! A dial (the client direction; the built-in [`RejectAll`]
|
//! A dial (the client direction; the built-in [`RejectAll`]
|
||||||
//! host-key policy connects to nothing — by design, the assembly's
|
//! host-key policy connects to nothing, by design, and the assembly's
|
||||||
//! policy replaces it):
|
//! policy replaces it):
|
||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
//! # {
|
//! # {
|
||||||
//! # let _ = conn.exec("uptime"); // the op rows (feature-gated)
|
//! # let _ = conn.exec("uptime"); // the op rows (feature-gated)
|
||||||
//! # }
|
//! # }
|
||||||
//! conn.close().await; // clean disconnect — drop is never the clean path
|
//! conn.close().await; // clean disconnect; drop is never the clean path
|
||||||
//! # Ok::<(), alkssh::DialError>(())
|
//! # Ok::<(), alkssh::DialError>(())
|
||||||
//! # })?;
|
//! # })?;
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
@@ -225,7 +225,7 @@ pub use client::dial;
|
|||||||
pub use client::russh_client_config;
|
pub use client::russh_client_config;
|
||||||
/// The `-R` leg's acceptor registry + the reverse-listen rows'
|
/// The `-R` leg's acceptor registry + the reverse-listen rows'
|
||||||
/// payload/acceptor types + the listener handles (`w4-reverse-listen`
|
/// payload/acceptor types + the listener handles (`w4-reverse-listen`
|
||||||
/// / `w4-lifecycle` — under `tunnel`; the recorded decisions live in
|
/// / `w4-lifecycle`, under `tunnel`; the recorded decisions live in
|
||||||
/// the `client_accept` module docs).
|
/// the `client_accept` module docs).
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
pub use client_accept::{
|
pub use client_accept::{
|
||||||
@@ -235,14 +235,14 @@ pub use client_accept::{
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use client_handler::ClientHandler;
|
pub use client_handler::ClientHandler;
|
||||||
/// The client op rows' surfaces (`w4-op-rows`):
|
/// The client op rows' surfaces (`w4-op-rows`):
|
||||||
/// [`RemoteSession`] — the exec/pty rows'
|
/// [`RemoteSession`]; the exec/pty rows'
|
||||||
/// family chunk plane (under `session`),
|
/// family chunk plane (under `session`),
|
||||||
/// [`TunnelChannel`] — the direct
|
/// [`TunnelChannel`]; the direct
|
||||||
/// rows' byte duplex (under `tunnel`),
|
/// rows' byte duplex (under `tunnel`),
|
||||||
/// [`GitPackSession`] — the upstream git
|
/// [`GitPackSession`]; the upstream git
|
||||||
/// row's pack duplex + consumed exit (under `git`). The op methods
|
/// row's pack duplex + consumed exit (under `git`). The op methods
|
||||||
/// live on [`SshConnection`] (the op-row surface). Not a re-export
|
/// live on [`SshConnection`] (the op-row surface). Not a re-export
|
||||||
/// here: the `sftp` row is [`SshConnection::sftp`] itself — the
|
/// here: the `sftp` row is [`SshConnection::sftp`] itself, the
|
||||||
/// every-build OQ-13 decline placeholder.
|
/// every-build OQ-13 decline placeholder.
|
||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
pub use client_ops::GitPackSession;
|
pub use client_ops::GitPackSession;
|
||||||
@@ -258,7 +258,7 @@ pub use config::SessionSeam;
|
|||||||
pub use config::TunnelListenSeam;
|
pub use config::TunnelListenSeam;
|
||||||
pub use config::{DialConfig, Limits, ServeConfig};
|
pub use config::{DialConfig, Limits, ServeConfig};
|
||||||
/// Fuzz-visible surface (`#[doc(hidden)]`, `w5-fuzz-harness`): the exec
|
/// Fuzz-visible surface (`#[doc(hidden)]`, `w5-fuzz-harness`): the exec
|
||||||
/// grammar's parse path (`dispatch.rs`) — the `fuzz/` sidecar's
|
/// grammar's parse path (`dispatch.rs`); the `fuzz/` sidecar's
|
||||||
/// `exec_parse` target drives the production parse, never a fork. Not
|
/// `exec_parse` target drives the production parse, never a fork. Not
|
||||||
/// documented API: the grammar is the dispatch seam's, not a public-API
|
/// documented API: the grammar is the dispatch seam's, not a public-API
|
||||||
/// promise.
|
/// promise.
|
||||||
@@ -267,7 +267,7 @@ pub use dispatch::{ExecDecline, ExecRoute, GitServiceRoute, parse_exec};
|
|||||||
pub use error::{AdapterError, DialError, KeySourceError, OpError, OpenDeclineReason};
|
pub use error::{AdapterError, DialError, KeySourceError, OpError, OpenDeclineReason};
|
||||||
pub use fingerprint::fingerprint;
|
pub use fingerprint::fingerprint;
|
||||||
/// Contract-test surface (`#[doc(hidden)]`): the per-connection auth
|
/// Contract-test surface (`#[doc(hidden)]`): the per-connection auth
|
||||||
/// bridge handler — constructible so the tests drive its hooks directly
|
/// bridge handler, constructible so the tests drive its hooks directly
|
||||||
/// against the wave-1 doubles (see `handler.rs` §Test surface). Not
|
/// against the wave-1 doubles (see `handler.rs` §Test surface). Not
|
||||||
/// documented API: the wire-facing surface is exactly `SshAdapter` +
|
/// documented API: the wire-facing surface is exactly `SshAdapter` +
|
||||||
/// the config/error/host/key-source types.
|
/// the config/error/host/key-source types.
|
||||||
@@ -281,7 +281,7 @@ pub use host_key::{HostKeyPolicy, PresentedHostKey, RejectAll};
|
|||||||
pub use key_source::decode_key_arm;
|
pub use key_source::decode_key_arm;
|
||||||
pub use key_source::{HostKeySource, KeySource, VaultHostKeySource, VaultKeySource};
|
pub use key_source::{HostKeySource, KeySource, VaultHostKeySource, VaultKeySource};
|
||||||
/// Contract-test surface (`#[doc(hidden)]`): the spawn-and-return
|
/// Contract-test surface (`#[doc(hidden)]`): the spawn-and-return
|
||||||
/// plumbing (`ADR-006 decision 4`, see `serve.rs`) — the mechanics the
|
/// plumbing (`ADR-006 decision 4`, see `serve.rs`), the mechanics the
|
||||||
/// mechanics-proof rows in `tests/spawn_return.rs` compose over the
|
/// mechanics-proof rows in `tests/spawn_return.rs` compose over the
|
||||||
/// real wire to pin the constraint as tests. Not documented API.
|
/// real wire to pin the constraint as tests. Not documented API.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|||||||
+57
-57
@@ -1,5 +1,5 @@
|
|||||||
//! The decline-path + serving-path event vocabulary
|
//! The decline-path + serving-path event vocabulary
|
||||||
//! (`w3-decline-tracing` — the wave-2 gate's booked finding): the
|
//! (`w3-decline-tracing`, the wave-2 gate's booked finding): the
|
||||||
//! observability surface every decline site in `src/` emits through,
|
//! observability surface every decline site in `src/` emits through,
|
||||||
//! and the serving-path lifecycle skeleton ([`serving_path_started`] /
|
//! and the serving-path lifecycle skeleton ([`serving_path_started`] /
|
||||||
//! [`serving_path_ended`] / [`serving_path_failed`]) the wave-3
|
//! [`serving_path_ended`] / [`serving_path_failed`]) the wave-3
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
//!
|
//!
|
||||||
//! ## The family posture read (2026-10-28)
|
//! ## The family posture read (2026-10-28)
|
||||||
//!
|
//!
|
||||||
//! Read before choosing this shape — no alkssh-private dialect:
|
//! Read before choosing this shape; no alkssh-private dialect:
|
||||||
//!
|
//!
|
||||||
//! - `alktty` (`adapter.rs`, `session.rs`, `local/pipe.rs`,
|
//! - `alktty` (`adapter.rs`, `session.rs`, `local/pipe.rs`,
|
||||||
//! `local/pty.rs`): free `tracing::{debug, warn}` imports,
|
//! `local/pty.rs`): free `tracing::{debug, warn}` imports,
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
//! (`listen = …`, `error = %e`, `reason = …`), levels `debug` for
|
//! (`listen = …`, `error = %e`, `reason = …`), levels `debug` for
|
||||||
//! structural observations, `warn` for recoverable failures,
|
//! structural observations, `warn` for recoverable failures,
|
||||||
//! `error` for abnormal pump terminations.
|
//! `error` for abnormal pump terminations.
|
||||||
//! - `alkcall` (`protocol/*`, `channels/*`): the widest use — the same
|
//! - `alkcall` (`protocol/*`, `channels/*`): the widest use, with the same
|
||||||
//! fields convention (`channel_id`, `request_id`, `error = %e`),
|
//! fields convention (`channel_id`, `request_id`, `error = %e`),
|
||||||
//! component-prefixed messages (`"channel client: …"`,
|
//! component-prefixed messages (`"channel client: …"`,
|
||||||
//! `"open wrapper: …"`), `debug`/`warn` only.
|
//! `"open wrapper: …"`), `debug`/`warn` only.
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
//! - `alkgit`: no tracing at all at this writing.
|
//! - `alkgit`: no tracing at all at this writing.
|
||||||
//!
|
//!
|
||||||
//! Never observed in the family: `info!` / `trace!` events, subscriber
|
//! Never observed in the family: `info!` / `trace!` events, subscriber
|
||||||
//! setup in library code, macro or span wrappers — so none here
|
//! setup in library code, macro or span wrappers, so none here
|
||||||
//! either. Adopted verbatim: free `tracing` fn calls, message-last
|
//! either. Adopted verbatim: free `tracing` fn calls, message-last
|
||||||
//! lowercase with the `"decline: …"` / `"serving path …"` phrasing,
|
//! lowercase with the `"decline: …"` / `"serving path …"` phrasing,
|
||||||
//! flat per-event fields (`feature_build`, `row`, `channel_id`,
|
//! flat per-event fields (`feature_build`, `row`, `channel_id`,
|
||||||
@@ -37,91 +37,91 @@
|
|||||||
//!
|
//!
|
||||||
//! Every event carries exactly:
|
//! Every event carries exactly:
|
||||||
//!
|
//!
|
||||||
//! - `feature_build = FEATURE_BUILD` — the crate's compile-time
|
//! - `feature_build = FEATURE_BUILD`: the crate's compile-time
|
||||||
//! feature set as a stable string (`"none"`, `"git"`,
|
//! feature set as a stable string (`"none"`, `"git"`,
|
||||||
//! `"session+git"`, …; features joined in the order `session, git,
|
//! `"session+git"`, …; features joined in the order `session, git,
|
||||||
//! tunnel, sftp`), so a captured event is self-describing per
|
//! tunnel, sftp`), so a captured event is self-describing per
|
||||||
//! build. The `sftp` feature gates no route, but ships in the
|
//! build. The `sftp` feature gates no route, but ships in the
|
||||||
//! build string: it is part of what the assembly deployed.
|
//! build string: it is part of what the assembly deployed.
|
||||||
//! - `row` — the greppable wire row the event is about: the channel
|
//! - `row`: the greppable wire row the event is about, the channel
|
||||||
//! type for open-level (e.g. `"session"`, `"direct-tcpip"`, `"x11"`),
|
//! type for open-level (e.g. `"session"`, `"direct-tcpip"`, `"x11"`),
|
||||||
//! the request name for request-level (e.g. `"pty_request"`,
|
//! the request name for request-level (e.g. `"pty_request"`,
|
||||||
//! `"tcpip_forward"`), the handler hook name for auth (e.g.
|
//! `"tcpip_forward"`), the handler hook name for auth (e.g.
|
||||||
//! `"auth_publickey"`), the session-row hook names for the
|
//! `"auth_publickey"`), the session-row hook names for the
|
||||||
//! serving-path lifecycle.
|
//! serving-path lifecycle.
|
||||||
//! - `channel_id` — the wire channel id (u32), on every event of a
|
//! - `channel_id`: the wire channel id (u32), on every event of a
|
||||||
//! channel; the auth class has none (the auth hooks carry no
|
//! channel; the auth class has none (the auth hooks carry no
|
||||||
//! channel at this surface).
|
//! channel at this surface).
|
||||||
//!
|
//!
|
||||||
//! Decline events additionally carry exactly:
|
//! Decline events additionally carry exactly:
|
||||||
//!
|
//!
|
||||||
//! - `decline_class` — `"open"` | `"request"` | `"serving"` |
|
//! - `decline_class`: `"open"` | `"request"` | `"serving"` |
|
||||||
//! `"arrival"` | `"auth"`:
|
//! `"arrival"` | `"auth"`:
|
||||||
//! the two wire decline classes of `mapping.md` §Wire decline
|
//! the two wire decline classes of `mapping.md` §Wire decline
|
||||||
//! reasons, plus the serving-path class (this wave the seam stub's
|
//! reasons, plus the serving-path class (this wave the seam stub's
|
||||||
//! decline *is* the serving path; the wave-3 mappings emit the same
|
//! decline *is* the serving path; the wave-3 mappings emit the same
|
||||||
//! class from their real spawned paths) and the auth class (the
|
//! class from their real spawned paths) and the auth class (the
|
||||||
//! disposition declines of ADR-004).
|
//! disposition declines of ADR-004).
|
||||||
//! - `reason` — the decline cause, mirroring the architecture's own
|
//! - `reason`: the decline cause, mirroring the architecture's own
|
||||||
//! classifications: `"unfeatured"` (feature-gate absence),
|
//! classifications: `"unfeatured"` (feature-gate absence),
|
||||||
//! `"never-feature"` (ADR-003 §5 rows), `"cap-exceeded"`,
|
//! `"never-feature"` (ADR-003 §5 rows), `"cap-exceeded"`,
|
||||||
//! `"connect-failed"` (`w3-tunnel-direct`: the tunnel rows'
|
//! `"connect-failed"` (`w3-tunnel-direct`: the tunnel rows'
|
||||||
//! cannot-bridge class — the failed fabric open, the wave-2 seam
|
//! cannot-bridge class: the failed fabric open, the wave-2 seam
|
||||||
//! stub's reason surviving as the real decline; also the
|
//! stub's reason surviving as the real decline; also the
|
||||||
//! `forwarded-tcpip` stub's standing decline class),
|
//! `forwarded-tcpip` stub's standing decline class),
|
||||||
//! `"seam-unconfigured"` (the config-driven row decline — the git
|
//! `"seam-unconfigured"` (the config-driven row decline: the git
|
||||||
//! row and, now, the session rows (feature on, seam unset;
|
//! row and, now, the session rows (feature on, seam unset;
|
||||||
//! `w3-git-mapping` / `w3-tty-session`) and the tunnel rows' absent
|
//! `w3-git-mapping` / `w3-tty-session`) and the tunnel rows' absent
|
||||||
//! fabric handle even under the feature (`w3-tunnel-direct`; the
|
//! fabric handle even under the feature (`w3-tunnel-direct`; the
|
||||||
//! unset-declines posture)),
|
//! unset-declines posture)),
|
||||||
//! `"shell-unconfigured"` / `"wrapper-unconfigured"` (`w3-tty-
|
//! `"shell-unconfigured"` / `"wrapper-unconfigured"` (`w3-tty-
|
||||||
//! session`: the OQ-08 unset-field rows — feature on, the
|
//! session`: the OQ-08 unset-field rows (feature on, the
|
||||||
//! serve-config field unset, never a fallback),
|
//! serve-config field unset, never a fallback),
|
||||||
//! `"negotiation-failed"` (`w3-tty-session`: the alktty
|
//! `"negotiation-failed"` (`w3-tty-session`: the alktty
|
||||||
//! negotiation error frame the bridge received — the session never
|
//! negotiation error frame the bridge received; the session never
|
||||||
//! started; the frame's JSON detail stays alktty-side),
|
//! started; the frame's JSON detail stays alktty-side),
|
||||||
//! `"unknown-service"` / `"service-only"` / `"invalid-repo-id"`
|
//! `"unknown-service"` / `"service-only"` / `"invalid-repo-id"`
|
||||||
//! (the exec grammar's git declinables — the parse core's two and
|
//! (the exec grammar's git declinables: the parse core's two and
|
||||||
//! the git row's validation gate; no service token or repo string
|
//! the git row's validation gate; no service token or repo string
|
||||||
//! ever rides — only the vocabulary value),
|
//! ever rides, only the vocabulary value),
|
||||||
//! `"exec-oversize"` (the pre-parse `max_exec_command_len` cap),
|
//! `"exec-oversize"` (the pre-parse `max_exec_command_len` cap),
|
||||||
//! `"invalid-target"` (`w3-tunnel-direct`: the direct rows'
|
//! `"invalid-target"` (`w3-tunnel-direct`: the direct rows'
|
||||||
//! impossible wire target — an out-of-range `direct-tcpip` port),
|
//! impossible wire target, an out-of-range `direct-tcpip` port),
|
||||||
//! `"unix-unavailable"` (`w3-tunnel-direct`: the OQ-11 runtime-Unix
|
//! `"unix-unavailable"` (`w3-tunnel-direct`: the OQ-11 runtime-Unix
|
||||||
//! gate — the off-Unix `direct-streamlocal` decline, answered
|
//! gate: the off-Unix `direct-streamlocal` decline, answered
|
||||||
//! before any fabric call),
|
//! before any fabric call),
|
||||||
//! `"no-identity"` / `"channel-not-held"` (the git row's serving-
|
//! `"no-identity"` / `"channel-not-held"` (the git row's serving-
|
||||||
//! path guards — unreachable in practice, fail-closed; the tunnel
|
//! path guards (unreachable in practice, fail-closed); the tunnel
|
||||||
//! rows' open-level `no-identity` guard rides the same value —
|
//! rows' open-level `no-identity` guard rides the same value,
|
||||||
//! reachable never, on the wire, for the same reason),
|
//! reachable never, on the wire, for the same reason),
|
||||||
//! `"unimplemented"` (the subsystem rows — OQ-13),
|
//! `"unimplemented"` (the subsystem rows, OQ-13),
|
||||||
//! `"declined-by-design"` (env, OQ-09),
|
//! `"declined-by-design"` (env, OQ-09),
|
||||||
//! `"correlation-miss"` / `"cap-exceeded"` / `"accept-refused"` /
|
//! `"correlation-miss"` / `"cap-exceeded"` / `"accept-refused"` /
|
||||||
//! `"unvanilla-inbound"` (`w3-tunnel-listen`: the `-R` leg's arrival
|
//! `"unvanilla-inbound"` (`w3-tunnel-listen`: the `-R` leg's arrival
|
||||||
//! declines — the class whose events carry the minted `listen` token
|
//! declines, the class whose events carry the minted `listen` token
|
||||||
//! instead of a channel id — and the unvanilla inbound
|
//! instead of a channel id, and the unvanilla inbound
|
||||||
//! `forwarded-tcpip` open),
|
//! `forwarded-tcpip` open),
|
||||||
//! `"listen-failed"` / `"no-such-listen"` (`w3-tunnel-listen`: the
|
//! `"listen-failed"` / `"no-such-listen"` (`w3-tunnel-listen`: the
|
||||||
//! listen dial's failed/bounded outcome and the cancel forms'
|
//! listen dial's failed/bounded outcome and the cancel forms'
|
||||||
//! not-found pair) — the global-request and arrival rows, and on the
|
//! not-found pair), belonging to the global-request and arrival
|
||||||
//! auth class `"method-unimplemented"` / `"cert-unsupported"` /
|
//! rows; on the auth class, `"method-unimplemented"` / `"cert-unsupported"` /
|
||||||
//! `"non-ed25519"` / `"fingerprint-unresolved"`.
|
//! `"non-ed25519"` / `"fingerprint-unresolved"`.
|
||||||
//! Client-side (the dial handler's `server_channel_open_*`
|
//! Client-side (the dial handler's `server_channel_open_*`
|
||||||
//! skeleton, `w4-dial`): `"no-acceptor"` (the decline-all default —
|
//! skeleton, `w4-dial`): `"no-acceptor"` (the decline-all default:
|
||||||
//! a server-initiated open with no registered acceptor; the
|
//! a server-initiated open with no registered acceptor; the
|
||||||
//! registry is `w4-reverse-listen`'s) and `"never-feature"` (the
|
//! registry is `w4-reverse-listen`'s) and `"never-feature"` (the
|
||||||
//! x11/agent rows — ADR-003 §5).
|
//! x11/agent rows, ADR-003 §5).
|
||||||
//! - `exec_route` — only on `exec_request`-row events: the dispatch
|
//! - `exec_route` rides only `exec_request`-row events: the dispatch
|
||||||
//! route the parse routed to (`"git"` | `"pipe"`), so a git row's
|
//! route the parse routed to (`"git"` | `"pipe"`), so a git row's
|
||||||
//! decline is distinguishable from a pipe row's in combined builds.
|
//! decline is distinguishable from a pipe row's in combined builds.
|
||||||
//! Other rows carry none (a `None` field is not recorded by
|
//! Other rows carry none (a `None` field is not recorded by
|
||||||
//! `tracing`).
|
//! `tracing`).
|
||||||
//!
|
//!
|
||||||
//! Levels: declines and the lifecycle's started/ended emit at
|
//! Levels: declines and the lifecycle's started/ended emit at
|
||||||
//! `debug` — a decline is the door's normal, honest posture (vanilla
|
//! `debug`: a decline is the door's normal, honest posture (vanilla
|
||||||
//! clients probe unfeatured rows routinely; nothing abnormal
|
//! clients probe unfeatured rows routinely; nothing abnormal
|
||||||
//! happened), matching the family's structural-observation level.
|
//! happened), matching the family's structural-observation level.
|
||||||
//! Only [`serving_path_failed`] is `warn` — a serving path that could
|
//! Only [`serving_path_failed`] is `warn`: a serving path that could
|
||||||
//! not complete its wire reporting is a real failure (the family's
|
//! not complete its wire reporting is a real failure (the family's
|
||||||
//! recoverable-failure level).
|
//! recoverable-failure level).
|
||||||
//!
|
//!
|
||||||
@@ -129,11 +129,11 @@
|
|||||||
//!
|
//!
|
||||||
//! **No secret material ever enters an event; never the username.**
|
//! **No secret material ever enters an event; never the username.**
|
||||||
//! Structurally enforced, not recalled: the helpers take only
|
//! Structurally enforced, not recalled: the helpers take only
|
||||||
//! `&'static str` vocabulary values and the wire channel id — there
|
//! `&'static str` vocabulary values and the wire channel id; there
|
||||||
//! is no parameter a username, command string, env name/value, repo
|
//! is no parameter a username, command string, env name/value, repo
|
||||||
//! path, key, or fingerprint could ride. In particular *the
|
//! path, key, or fingerprint could ride. In particular *the
|
||||||
//! fingerprint stays out of the events* (`auth.md` §Dispositions: log
|
//! fingerprint stays out of the events* (`auth.md` §Dispositions: log
|
||||||
//! exposure is deployment policy — the crate's events are
|
//! exposure is deployment policy; the crate's events are
|
||||||
//! safe-by-default at every level, so the fingerprint is a
|
//! safe-by-default at every level, so the fingerprint is a
|
||||||
//! deployer-side computation over its own logs, never crate-emitted).
|
//! deployer-side computation over its own logs, never crate-emitted).
|
||||||
//! The `error` field carries only russh protocol errors. The wire
|
//! The `error` field carries only russh protocol errors. The wire
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
//! appears in any captured field. If a future decline site needs
|
//! appears in any captured field. If a future decline site needs
|
||||||
//! request-argument detail (e.g. the git grammar's decline reasons in
|
//! request-argument detail (e.g. the git grammar's decline reasons in
|
||||||
//! `w3-git-mapping`), it must add an explicitly-vocabulary'd field and
|
//! `w3-git-mapping`), it must add an explicitly-vocabulary'd field and
|
||||||
//! record its safety reasoning — dynamic strings do not ride this
|
//! record its safety reasoning; dynamic strings do not ride this
|
||||||
//! module by default.
|
//! module by default.
|
||||||
|
|
||||||
/// The compile-time feature set, as a stable string (`"none"`,
|
/// The compile-time feature set, as a stable string (`"none"`,
|
||||||
@@ -194,7 +194,7 @@ pub(crate) fn decline_open(row: &'static str, reason: &'static str, channel_id:
|
|||||||
/// build does not serve (`subsystem` / `env` / `x11` / agent, an
|
/// build does not serve (`subsystem` / `env` / `x11` / agent, an
|
||||||
/// unfeatured parse route) or a not-yet-landed listen form. The wire
|
/// unfeatured parse route) or a not-yet-landed listen form. The wire
|
||||||
/// answer is `channel_failure` (the channel stays usable) or the
|
/// answer is `channel_failure` (the channel stays usable) or the
|
||||||
/// denied-request reply — the event is the deciding context.
|
/// denied-request reply; the event is the deciding context.
|
||||||
pub(crate) fn decline_request(
|
pub(crate) fn decline_request(
|
||||||
row: &'static str,
|
row: &'static str,
|
||||||
reason: &'static str,
|
reason: &'static str,
|
||||||
@@ -214,7 +214,7 @@ pub(crate) fn decline_request(
|
|||||||
|
|
||||||
/// An auth-class decline: a disposition of the ADR-004 bridge turned
|
/// An auth-class decline: a disposition of the ADR-004 bridge turned
|
||||||
/// the method away. No channel id exists at this surface; the
|
/// the method away. No channel id exists at this surface; the
|
||||||
/// *fingerprint never rides the event* (see §Safety posture) — the
|
/// *fingerprint never rides the event* (see §Safety posture); the
|
||||||
/// `reason` codes distinguish the decline causes without exposing who.
|
/// `reason` codes distinguish the decline causes without exposing who.
|
||||||
pub(crate) fn decline_auth(row: &'static str, reason: &'static str) {
|
pub(crate) fn decline_auth(row: &'static str, reason: &'static str) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
@@ -226,7 +226,7 @@ pub(crate) fn decline_auth(row: &'static str, reason: &'static str) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A request-level decline on a **global** request — the `-R` listen
|
/// A request-level decline on a **global** request: the `-R` listen
|
||||||
/// forms (`mapping.md` §Global-request rows). No channel id exists at
|
/// forms (`mapping.md` §Global-request rows). No channel id exists at
|
||||||
/// this surface; the fields are otherwise the `request` vocabulary.
|
/// this surface; the fields are otherwise the `request` vocabulary.
|
||||||
pub(crate) fn decline_global_request(row: &'static str, reason: &'static str) {
|
pub(crate) fn decline_global_request(row: &'static str, reason: &'static str) {
|
||||||
@@ -241,12 +241,12 @@ pub(crate) fn decline_global_request(row: &'static str, reason: &'static str) {
|
|||||||
|
|
||||||
/// A decline on the `-R` leg's **arrival** surface (`w3-tunnel-
|
/// A decline on the `-R` leg's **arrival** surface (`w3-tunnel-
|
||||||
/// listen`): a fabric inbound forwarded open that cannot become an
|
/// listen`): a fabric inbound forwarded open that cannot become an
|
||||||
/// SSH channel — `correlation-miss` (no route for the minted token —
|
/// SSH channel: `correlation-miss` (no route for the minted token,
|
||||||
/// teardown-at-birth), `cap-exceeded` (the per-arrival admission), or
|
/// teardown-at-birth), `cap-exceeded` (the per-arrival admission), or
|
||||||
/// `accept-refused` (the vanilla client rejected the SSH open — the
|
/// `accept-refused` (the vanilla client rejected the SSH open; the
|
||||||
/// count released, never a half-open channel). No SSH channel id
|
/// count released, never a half-open channel). No SSH channel id
|
||||||
/// exists yet at this surface (that is the point of the declines), so
|
/// exists yet at this surface (that is the point of the declines), so
|
||||||
/// the minted `listen` token — the correlation key itself — rides
|
/// the minted `listen` token (the correlation key itself) rides
|
||||||
/// instead; the token is bookkeeping metadata (the producer's minted
|
/// instead; the token is bookkeeping metadata (the producer's minted
|
||||||
/// correlation number), never secret material.
|
/// correlation number), never secret material.
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
@@ -265,7 +265,7 @@ pub(crate) fn decline_arrival(row: &'static str, reason: &'static str, listen: u
|
|||||||
/// failure-then-close rather than serving. This wave that is the seam
|
/// failure-then-close rather than serving. This wave that is the seam
|
||||||
/// stub (the wire shape pinned in `tests/spawn_return.rs`); the wave-3
|
/// stub (the wire shape pinned in `tests/spawn_return.rs`); the wave-3
|
||||||
/// mappings emit it from their real spawned paths wherever serving
|
/// mappings emit it from their real spawned paths wherever serving
|
||||||
/// itself cannot proceed — the class the spawned paths inherit.
|
/// itself cannot proceed, the class the spawned paths inherit.
|
||||||
#[cfg(any(feature = "git", feature = "session"))]
|
#[cfg(any(feature = "git", feature = "session"))]
|
||||||
pub(crate) fn decline_serving(
|
pub(crate) fn decline_serving(
|
||||||
row: &'static str,
|
row: &'static str,
|
||||||
@@ -284,7 +284,7 @@ pub(crate) fn decline_serving(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawned-path started — emitted as the spawned task's first action,
|
/// Spawned-path started, emitted as the spawned task's first action,
|
||||||
/// so "started" is the task's own honest start, not the hook's spawn
|
/// so "started" is the task's own honest start, not the hook's spawn
|
||||||
/// call. The mapping tasks' serving paths inherit this shape.
|
/// call. The mapping tasks' serving paths inherit this shape.
|
||||||
#[cfg(any(feature = "git", feature = "session", feature = "tunnel"))]
|
#[cfg(any(feature = "git", feature = "session", feature = "tunnel"))]
|
||||||
@@ -302,7 +302,7 @@ pub(crate) fn serving_path_started(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawned-path ended — the serving future finished (cleanly, or with
|
/// Spawned-path ended: the serving future finished (cleanly, or with
|
||||||
/// the decline it was wired to answer); emitted *after* the path's
|
/// the decline it was wired to answer); emitted *after* the path's
|
||||||
/// wire reporting has completed.
|
/// wire reporting has completed.
|
||||||
#[cfg(any(feature = "git", feature = "session", feature = "tunnel"))]
|
#[cfg(any(feature = "git", feature = "session", feature = "tunnel"))]
|
||||||
@@ -320,14 +320,14 @@ pub(crate) fn serving_path_ended(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawned-path failure — the serving path could not complete its
|
/// Spawned-path failure: the serving path could not complete its
|
||||||
/// wire reporting (the session handle's sends failed: the session is
|
/// wire reporting (the session handle's sends failed: the session is
|
||||||
/// gone, or the report never landed). The vocabulary's one `warn` —
|
/// gone, or the report never landed). The vocabulary's one `warn`,
|
||||||
/// the family's recoverable-failure level. Only russh protocol errors
|
/// the family's recoverable-failure level. Only russh protocol errors
|
||||||
/// ride the `error` field (§Safety posture); the mapping tasks emit
|
/// ride the `error` field (§Safety posture); the mapping tasks emit
|
||||||
/// this from their real paths when serving fails before reporting.
|
/// this from their real paths when serving fails before reporting.
|
||||||
/// (The tunnel bridge never calls this: its wire reporting is
|
/// (The tunnel bridge never calls this: its wire reporting is
|
||||||
/// open-level — `accept`/`reject` cannot fail send-wise — so the fn
|
/// open-level (`accept`/`reject` cannot fail send-wise), so the fn
|
||||||
/// stays gated to the git/session rows.)
|
/// stays gated to the git/session rows.)
|
||||||
#[cfg(any(feature = "git", feature = "session"))]
|
#[cfg(any(feature = "git", feature = "session"))]
|
||||||
pub(crate) fn serving_path_failed<E: std::fmt::Debug>(
|
pub(crate) fn serving_path_failed<E: std::fmt::Debug>(
|
||||||
@@ -350,7 +350,7 @@ pub(crate) fn serving_path_failed<E: std::fmt::Debug>(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use tracing::subscriber::with_default;
|
use tracing::subscriber::with_default;
|
||||||
|
|
||||||
/// A fake error shape — the failure row only pins that a Debug
|
/// A fake error shape; the failure row only pins that a Debug
|
||||||
/// error value rides the `error` field.
|
/// error value rides the `error` field.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[cfg(any(feature = "git", feature = "session"))]
|
#[cfg(any(feature = "git", feature = "session"))]
|
||||||
@@ -362,10 +362,10 @@ mod tests {
|
|||||||
|
|
||||||
/// A minimal field-capturing visitor shared by these rows and the
|
/// A minimal field-capturing visitor shared by these rows and the
|
||||||
/// wire rows in `tests/observe.rs` (the test convention recorded
|
/// wire rows in `tests/observe.rs` (the test convention recorded
|
||||||
/// in the task notes — the family has no event-capture precedent,
|
/// in the task notes: the family has no event-capture precedent,
|
||||||
/// only alktunnels' fmt-subscriber dev-dep for debug output, so
|
/// only alktunnels' fmt-subscriber dev-dep for debug output, so
|
||||||
/// the convention is a hand-rolled minimal [`tracing::Subscriber`]
|
/// the convention is a hand-rolled minimal [`tracing::Subscriber`]
|
||||||
/// — no `tracing-subscriber` dependency).
|
/// with no `tracing-subscriber` dependency).
|
||||||
struct Visitor(Vec<(String, String)>);
|
struct Visitor(Vec<(String, String)>);
|
||||||
|
|
||||||
impl tracing::field::Visit for Visitor {
|
impl tracing::field::Visit for Visitor {
|
||||||
@@ -379,7 +379,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Captures the fields of the one event the emit fn fires, via
|
/// Captures the fields of the one event the emit fn fires, via
|
||||||
/// the thread-local default subscriber (`with_default` — these
|
/// the thread-local default subscriber (`with_default`; these
|
||||||
/// are direct helper rows; the spawned-task surfaces are driven
|
/// are direct helper rows; the spawned-task surfaces are driven
|
||||||
/// over the wire in `tests/observe.rs` against the global
|
/// over the wire in `tests/observe.rs` against the global
|
||||||
/// capture).
|
/// capture).
|
||||||
@@ -425,8 +425,8 @@ mod tests {
|
|||||||
.unwrap_or_else(|| panic!("no field {name} in {fields:?}"))
|
.unwrap_or_else(|| panic!("no field {name} in {fields:?}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the event vocabulary (`w3-decline-tracing`)
|
/// - *Contract stamp:* the event vocabulary (`w3-decline-tracing`):
|
||||||
/// — an open decline carries exactly the base shape, values
|
/// an open decline carries exactly the base shape, values
|
||||||
/// included.
|
/// included.
|
||||||
#[test]
|
#[test]
|
||||||
fn decline_open_carries_the_full_vocabulary() {
|
fn decline_open_carries_the_full_vocabulary() {
|
||||||
@@ -449,11 +449,11 @@ mod tests {
|
|||||||
"reason".to_owned(),
|
"reason".to_owned(),
|
||||||
],
|
],
|
||||||
"an open decline carries exactly the vocabulary, nothing else \
|
"an open decline carries exactly the vocabulary, nothing else \
|
||||||
(the message field first — the event! macro's shape)"
|
(the message field first, per the event! macro's shape)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the `exec_route` field — present on the
|
/// - *Contract stamp:* the `exec_route` field: present on the
|
||||||
/// exec row, absent elsewhere (a `None` field is not recorded
|
/// exec row, absent elsewhere (a `None` field is not recorded
|
||||||
/// by `tracing`).
|
/// by `tracing`).
|
||||||
#[test]
|
#[test]
|
||||||
@@ -480,7 +480,7 @@ mod tests {
|
|||||||
assert_eq!(field(&fields, "reason"), "method-unimplemented");
|
assert_eq!(field(&fields, "reason"), "method-unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the `exec_route` field — present on the
|
/// - *Contract stamp:* the `exec_route` field: present on the
|
||||||
/// exec row, absent elsewhere (a `None` field is not recorded
|
/// exec row, absent elsewhere (a `None` field is not recorded
|
||||||
/// by `tracing`). The pty/shell lifecycle rows carry no route.
|
/// by `tracing`). The pty/shell lifecycle rows carry no route.
|
||||||
#[cfg(any(feature = "git", feature = "session"))]
|
#[cfg(any(feature = "git", feature = "session"))]
|
||||||
@@ -503,9 +503,9 @@ mod tests {
|
|||||||
assert_eq!(field(&fields, "error"), "Probe");
|
assert_eq!(field(&fields, "error"), "Probe");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - *Contract stamp:* the arrival vocabulary (`w3-tunnel-listen`)
|
/// - *Contract stamp:* the arrival vocabulary (`w3-tunnel-listen`):
|
||||||
/// — an arrival decline carries the minted `listen` token
|
/// an arrival decline carries the minted `listen` token
|
||||||
/// instead of a channel id (none exists yet — that is the point
|
/// instead of a channel id (none exists yet; that is the point
|
||||||
/// of the declines; the token is bookkeeping metadata, never
|
/// of the declines; the token is bookkeeping metadata, never
|
||||||
/// secret material).
|
/// secret material).
|
||||||
#[cfg(feature = "tunnel")]
|
#[cfg(feature = "tunnel")]
|
||||||
@@ -530,7 +530,7 @@ mod tests {
|
|||||||
|
|
||||||
/// The feature-build string: every enabled feature appears; a
|
/// The feature-build string: every enabled feature appears; a
|
||||||
/// featureless build reads `none`. The expected half is derived
|
/// featureless build reads `none`. The expected half is derived
|
||||||
/// from `cfg!` independently of the crate's match — divergence
|
/// from `cfg!` independently of the crate's match, so divergence
|
||||||
/// breaks this row loudly.
|
/// breaks this row loudly.
|
||||||
#[test]
|
#[test]
|
||||||
fn feature_build_string_reflects_the_cfgs() {
|
fn feature_build_string_reflects_the_cfgs() {
|
||||||
|
|||||||
+11
-11
@@ -1,22 +1,22 @@
|
|||||||
//! The spawn-and-return plumbing (`w2-spawn-return`) — ADR-006
|
//! The spawn-and-return plumbing (`w2-spawn-return`): ADR-006
|
||||||
//! decision 4, the structural constraint of the whole spine, in code.
|
//! decision 4, the structural constraint of the whole spine, in code.
|
||||||
//!
|
//!
|
||||||
//! russh at this pin runs server handler callbacks **inline inside the
|
//! russh at this pin runs server handler callbacks **inline inside the
|
||||||
//! session's packet loop** — a blocking handler suspends all packet
|
//! session's packet loop**: a blocking handler suspends all packet
|
||||||
//! routing for the connection and deadlocks against the peer's
|
//! routing for the connection and deadlocks against the peer's
|
||||||
//! immediate follow-up bytes (POC-3 proved by construction with
|
//! immediate follow-up bytes (POC-3 proved by construction with
|
||||||
//! receive-pack). The crate's answer is mechanical, not conventional:
|
//! receive-pack). The crate's answer is mechanical, not conventional:
|
||||||
//!
|
//!
|
||||||
//! - **Every serving path is spawned** ([`spawn_serving_path`]) — the
|
//! - **Every serving path is spawned** ([`spawn_serving_path`]): the
|
||||||
//! handler hook clones the session handle
|
//! handler hook clones the session handle
|
||||||
//! (`Session::handle()`), hands it into its serving future, spawns,
|
//! (`Session::handle()`), hands it into its serving future, spawns,
|
||||||
//! and returns. No serving path is ever awaited inline; the dispatch
|
//! and returns. No serving path is ever awaited inline; the dispatch
|
||||||
//! seam stubs ([`crate::handler`]) hang off this helper, so the
|
//! seam stubs ([`crate::handler`]) hang off this helper, so the
|
||||||
//! wave-3 mappings inherit the pattern mechanically from the stubs.
|
//! wave-3 mappings inherit the pattern mechanically from the stubs.
|
||||||
//! The serving-path *events* (started / ended / failure —
|
//! The serving-path *events* (started / ended / failure, via
|
||||||
//! [`crate::observe`]) are the spawned task's own actions: the stub
|
//! [`crate::observe`]) are the spawned task's own actions: the stub
|
||||||
//! body emits them, the mappings' real paths will too.
|
//! body emits them, the mappings' real paths will too.
|
||||||
//! - **Exit/status reporting rides the cloned handle** — the spawned
|
//! - **Exit/status reporting rides the cloned handle**: the spawned
|
||||||
//! task's only report surface is the handle clone it was handed;
|
//! task's only report surface is the handle clone it was handed;
|
||||||
//! [`report_exit_status_then_close`] preserves the wire ordering the
|
//! [`report_exit_status_then_close`] preserves the wire ordering the
|
||||||
//! `mapping.md` `exit_status` row pins: `exit-status` sent **before**
|
//! `mapping.md` `exit_status` row pins: `exit-status` sent **before**
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
//!
|
//!
|
||||||
//! ## Test surface
|
//! ## Test surface
|
||||||
//!
|
//!
|
||||||
//! Both helpers are re-exported `#[doc(hidden)]` from the crate root —
|
//! Both helpers are re-exported `#[doc(hidden)]` from the crate root,
|
||||||
//! the `handler.rs` §Test surface pattern: the mechanics-proof rows in
|
//! the `handler.rs` §Test surface pattern: the mechanics-proof rows in
|
||||||
//! `tests/spawn_return.rs` compose them over the real wire (a minimal
|
//! `tests/spawn_return.rs` compose them over the real wire (a minimal
|
||||||
//! proof handler standing in for the crate's own auth/dispatch hooks,
|
//! proof handler standing in for the crate's own auth/dispatch hooks,
|
||||||
@@ -41,7 +41,7 @@ use russh::ChannelId;
|
|||||||
use russh::server::Handle;
|
use russh::server::Handle;
|
||||||
|
|
||||||
/// Spawns one serving path on its own task (ADR-006 decision 4): the
|
/// Spawns one serving path on its own task (ADR-006 decision 4): the
|
||||||
/// caller — the handler hook — clones the session handle, hands it
|
/// caller (the handler hook) clones the session handle, hands it
|
||||||
/// into the serving future, calls this, and returns immediately.
|
/// into the serving future, calls this, and returns immediately.
|
||||||
/// Nothing here is ever awaited inline: an inline serve deadlocks
|
/// Nothing here is ever awaited inline: an inline serve deadlocks
|
||||||
/// against the peer's follow-up bytes (POC-3 proved by construction).
|
/// against the peer's follow-up bytes (POC-3 proved by construction).
|
||||||
@@ -50,7 +50,7 @@ pub fn spawn_serving_path(serving: impl Future<Output = ()> + Send + 'static) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The spawned task's exit report through the cloned handle:
|
/// The spawned task's exit report through the cloned handle:
|
||||||
/// `exit-status(N)` then close — the ordering contract
|
/// `exit-status(N)` then close; the ordering contract
|
||||||
/// (`mapping.md`'s `exit_status` row: sent **before** close, from the
|
/// (`mapping.md`'s `exit_status` row: sent **before** close, from the
|
||||||
/// spawned task). A failed send means the session is gone; nothing is
|
/// spawned task). A failed send means the session is gone; nothing is
|
||||||
/// reportable then.
|
/// reportable then.
|
||||||
@@ -67,8 +67,8 @@ mod tests {
|
|||||||
use super::spawn_serving_path;
|
use super::spawn_serving_path;
|
||||||
|
|
||||||
/// - *Contract stamp:* ADR-006 decision 4 (spawn-and-return: the
|
/// - *Contract stamp:* ADR-006 decision 4 (spawn-and-return: the
|
||||||
/// handler call returns while the serving path is still running
|
/// handler call returns while the serving path is still running,
|
||||||
/// — handler-returns-fast is structural).
|
/// so handler-returns-fast is structural).
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn spawn_returns_while_the_serving_path_is_still_pending() {
|
async fn spawn_returns_while_the_serving_path_is_still_pending() {
|
||||||
let (release, waited) = tokio::sync::oneshot::channel::<()>();
|
let (release, waited) = tokio::sync::oneshot::channel::<()>();
|
||||||
@@ -82,7 +82,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!done.load(Ordering::Acquire),
|
!done.load(Ordering::Acquire),
|
||||||
"the spawn call returned while the serving path was still gated — \
|
"the spawn call returned while the serving path was still gated; \
|
||||||
no serving path is awaited inline"
|
no serving path is awaited inline"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+59
-58
@@ -1,11 +1,11 @@
|
|||||||
//! The session mapping (`w3-tty-session`) — the biggest single-row
|
//! The session mapping (`w3-tty-session`), the biggest single-row
|
||||||
//! surface of the spine ([`crate::dispatch::ExecRoute::Pipe`] + the
|
//! surface of the spine ([`crate::dispatch::ExecRoute::Pipe`] + the
|
||||||
//! `pty_request`/`shell_request` rows): pty/shell/pipe-exec serve
|
//! `pty_request`/`shell_request` rows): pty/shell/pipe-exec serve
|
||||||
//! through the published alktty seam
|
//! through the published alktty seam
|
||||||
//! (`drive_session_pre_negotiated(writer, reader, req, backends,
|
//! (`drive_session_pre_negotiated(writer, reader, req, backends,
|
||||||
//! ownership, identity)`) over a **translation bridge, not a byte
|
//! ownership, identity)`) over a **translation bridge, not a byte
|
||||||
//! pump** (POC-3 §D.1): alktty's data plane is chunk-framed both
|
//! pump** (POC-3 §D.1): alktty's data plane is chunk-framed both
|
||||||
//! directions (`alktty::wire` — 5-byte header + payload), the SSH
|
//! directions (`alktty::wire`, 5-byte header + payload), the SSH
|
||||||
//! channel is a plain byte plane, so the bridge wraps the channel
|
//! channel is a plain byte plane, so the bridge wraps the channel
|
||||||
//! with small adapters around a pair of in-memory duplexes.
|
//! with small adapters around a pair of in-memory duplexes.
|
||||||
//!
|
//!
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
//!
|
//!
|
||||||
//! - `pty_request` args → `alktty::negotiation::TerminalParamsWire`
|
//! - `pty_request` args → `alktty::negotiation::TerminalParamsWire`
|
||||||
//! (`term`/`cols`/`rows`/pixel dims; SSH ttymodes → the **reserved
|
//! (`term`/`cols`/`rows`/pixel dims; SSH ttymodes → the **reserved
|
||||||
//! modes JSON `{}`** — alktty OQ-44's reservation: backends must
|
//! modes JSON `{}`** per alktty OQ-44's reservation: backends must
|
||||||
//! ignore modes in v1, so the door writes `{}` and defers
|
//! ignore modes in v1, so the door writes `{}` and defers
|
||||||
//! faithfully). Stored per channel (the POC-3 accepted-but-held
|
//! faithfully). Stored per channel (the POC-3 accepted-but-held
|
||||||
//! shape, [`crate::handler`]'s store) and consumed by the later
|
//! shape, [`crate::handler`]'s store) and consumed by the later
|
||||||
@@ -24,29 +24,29 @@
|
|||||||
//! - `shell_request` → `NegotiateRequest{carriage: "raw", backend:
|
//! - `shell_request` → `NegotiateRequest{carriage: "raw", backend:
|
||||||
//! the seam selector, tty: the consumed pty params, cmd: the
|
//! the seam selector, tty: the consumed pty params, cmd: the
|
||||||
//! serve-config shell argv **verbatim**, cwd: session_cwd, env:
|
//! serve-config shell argv **verbatim**, cwd: session_cwd, env:
|
||||||
//! session_env}` — the wire never carries command, args, cwd or
|
//! session_env}`, and the wire never carries command, args, cwd or
|
||||||
//! env (OQ-08/OQ-09; the `env_request` row stays declined
|
//! env (OQ-08/OQ-09; the `env_request` row stays declined
|
||||||
//! unconditionally). **A `shell_request` without a preceding pty
|
//! unconditionally). **A `shell_request` without a preceding pty
|
||||||
//! serves pipe-mode (`tty = None`)** — decided: vanilla sshd serves
|
//! serves pipe-mode (`tty = None`)**, decided: vanilla sshd serves
|
||||||
//! a shell without a terminal too (`ssh -T host` is legitimate
|
//! a shell without a terminal too (`ssh -T host` is legitimate
|
||||||
//! vanilla), so a pty-less shell request is served, never declined.
|
//! vanilla), so a pty-less shell request is served, never declined.
|
||||||
//! - `exec_request` (pipe row) → `cmd = exec_wrapper_argv ++ [cmd]`
|
//! - `exec_request` (pipe row) → `cmd = exec_wrapper_argv ++ [cmd]`
|
||||||
//! (the OQ-08 argv append — vanilla quoting semantics; the wire
|
//! (the OQ-08 argv append, vanilla quoting semantics; the wire
|
||||||
//! command string is **never naively split**), `tty =
|
//! command string is **never naively split**), `tty =
|
||||||
//! Some(consumed pty)` when a pty preceded, else `None` (pipe
|
//! Some(consumed pty)` when a pty preceded, else `None` (pipe
|
||||||
//! mode).
|
//! mode).
|
||||||
//! - **Unset-declines** (OQ-08, never a fallback): `shell_argv`
|
//! - **Unset-declines** (OQ-08, never a fallback): `shell_argv`
|
||||||
//! unset → `shell_request` declines; `exec_wrapper_argv` unset →
|
//! unset → `shell_request` declines; `exec_wrapper_argv` unset →
|
||||||
//! the pipe row declines. Wire shape (decided + recorded): the
|
//! the pipe row declines. Wire shape (decided + recorded): the
|
||||||
//! **serving-path shape — `channel_failure` then channel close,
|
//! **serving-path shape: `channel_failure` then channel close,
|
||||||
//! from a spawned task** ([`crate::serve`]'s mechanics; the same
|
//! from a spawned task** ([`crate::serve`]'s mechanics; the same
|
||||||
//! spawned-stub body the git row uses) — the class of a routable
|
//! spawned-stub body the git row uses), the class of a routable
|
||||||
//! row that owns the request's whole outcome; the session channel
|
//! row that owns the request's whole outcome; the session channel
|
||||||
//! closes the way it would when the request itself cannot exist.
|
//! closes the way it would when the request itself cannot exist.
|
||||||
//! `session_seam` unset (feature on, seam absent) → the same
|
//! `session_seam` unset (feature on, seam absent) → the same
|
||||||
//! shape, reason `seam-unconfigured` (the decline matrix's
|
//! shape, reason `seam-unconfigured` (the decline matrix's
|
||||||
//! config-driven note); `pty_request` checks the seam too — a pty
|
//! config-driven note); `pty_request` checks the seam too, since a
|
||||||
//! whose session can never serve is a doomed allocation. Field
|
//! pty whose session can never serve is a doomed allocation. Field
|
||||||
//! checks precede the seam check (the row's own unset field is the
|
//! checks precede the seam check (the row's own unset field is the
|
||||||
//! first decline); the decline never takes the channel and never
|
//! first decline); the decline never takes the channel and never
|
||||||
//! consumes stored pty state.
|
//! consumes stored pty state.
|
||||||
@@ -63,10 +63,10 @@
|
|||||||
//! - **SSH → chunks** (the `client_recv` side of alktty's session):
|
//! - **SSH → chunks** (the `client_recv` side of alktty's session):
|
||||||
//! a bounded [`tokio::sync::mpsc`] (the
|
//! a bounded [`tokio::sync::mpsc`] (the
|
||||||
//! [`CONTROL_SURFACE_BOUND`] bound) feeds a framing task that
|
//! [`CONTROL_SURFACE_BOUND`] bound) feeds a framing task that
|
||||||
//! writes `alktty::wire::Chunk`s into the duplex — the same mpsc
|
//! writes `alktty::wire::Chunk`s into the duplex, the same mpsc
|
||||||
//! the control rows ride ([`BridgeSurface`], §below). SSH `data` →
|
//! the control rows ride ([`BridgeSurface`], §below). SSH `data` →
|
||||||
//! `Chunk::stdin`; SSH channel EOF → **the zero-length
|
//! `Chunk::stdin`; SSH channel EOF → **the zero-length
|
||||||
//! stdin-chunk sentinel** — decided (verified against alktty 0.8.1's
|
//! stdin-chunk sentinel**, decided (verified against alktty 0.8.1's
|
||||||
//! adapter pump, `adapter.rs::pump_client_to_backend`): a
|
//! adapter pump, `adapter.rs::pump_client_to_backend`): a
|
||||||
//! zero-length stdin chunk shuts the backend's stdin, *identical
|
//! zero-length stdin chunk shuts the backend's stdin, *identical
|
||||||
//! in effect* to `ControlMessage::Eof` on `STREAM_CTRL_IN`; the
|
//! in effect* to `ControlMessage::Eof` on `STREAM_CTRL_IN`; the
|
||||||
@@ -80,9 +80,9 @@
|
|||||||
//! *error frame* is length-prefixed JSON whose first length byte
|
//! *error frame* is length-prefixed JSON whose first length byte
|
||||||
//! is `0x00`; a raw chunk's first byte is a `stream_type` ≤ 4).
|
//! is `0x00`; a raw chunk's first byte is a `stream_type` ≤ 4).
|
||||||
//! stdout chunks → SSH `data`; stderr chunks → SSH
|
//! stdout chunks → SSH `data`; stderr chunks → SSH
|
||||||
//! `extended_data(1)` (PTY mode merges upstream — the backend's
|
//! `extended_data(1)` (PTY mode merges upstream; the backend's
|
||||||
//! stderr stream is `None` there, so the translation is universal:
|
//! stderr stream is `None` there, so the translation is universal
|
||||||
//! no mode-switch exists); the exit chunk (`STREAM_CTRL_OUT`) →
|
//! and no mode-switch exists); the exit chunk (`STREAM_CTRL_OUT`) →
|
||||||
//! the derivation contract (§below). No unbounded buffering in
|
//! the derivation contract (§below). No unbounded buffering in
|
||||||
//! either adapter (AGENTS.md convention 9).
|
//! either adapter (AGENTS.md convention 9).
|
||||||
//! - **The negotiation-error frame is the bridge's serving-path
|
//! - **The negotiation-error frame is the bridge's serving-path
|
||||||
@@ -91,23 +91,24 @@
|
|||||||
//! ownership), and a failure answers a length-prefixed error
|
//! ownership), and a failure answers a length-prefixed error
|
||||||
//! frame instead of chunks. Decided + recorded: the bridge maps
|
//! frame instead of chunks. Decided + recorded: the bridge maps
|
||||||
//! that frame to a serving-path decline event
|
//! that frame to a serving-path decline event
|
||||||
//! (`negotiation-failed`) and an **`exit-status(255)` then close**
|
//! (`negotiation-failed`) and an **`exit-status(255)` then close**,
|
||||||
//! — the one place this door claims 255: the session **never
|
//! the one place this door claims 255: the session **never
|
||||||
//! started** (no exit chunk exists, nothing ran, so 255 is the
|
//! started** (no exit chunk exists, nothing ran, so 255 is the
|
||||||
//! honest could-not-start semantic — contrast the git row, whose
|
//! honest could-not-start semantic, contrasted with the git row,
|
||||||
|
//! whose
|
||||||
//! service *runs* its substrate and honestly reports `1`). The
|
//! service *runs* its substrate and honestly reports `1`). The
|
||||||
//! error frame's JSON detail stays alktty-side; only the
|
//! error frame's JSON detail stays alktty-side; only the
|
||||||
//! vocabulary value rides the event.
|
//! vocabulary value rides the event.
|
||||||
//! - A bridge read that ends **without** an exit chunk reports
|
//! - A bridge read that ends **without** an exit chunk reports
|
||||||
//! nothing (the chunk is the sole fidelity input; its absence is
|
//! nothing (the chunk is the sole fidelity input; its absence is
|
||||||
//! neither 0 nor 255 — no fabricated report): plain close, and
|
//! neither 0 nor 255, no fabricated report): plain close, and
|
||||||
//! the lifecycle's ended event.
|
//! the lifecycle's ended event.
|
||||||
//! - **Known limit (recorded disposition, wave-3 gate booked item 3):
|
//! - **Known limit (recorded disposition, wave-3 gate booked item 3):
|
||||||
//! the stdout [`tokio::io::AsyncWriteExt::write_all`] carries no
|
//! the stdout [`tokio::io::AsyncWriteExt::write_all`] carries no
|
||||||
//! deadline.** Backpressure is bounded (the bounded bridging duplex,
|
//! deadline.** Backpressure is bounded (the bounded bridging duplex,
|
||||||
//! russh's per-channel window and the client's own flow control bound
|
//! russh's per-channel window and the client's own flow control bound
|
||||||
//! what can pile up) and liveness is covered by the inactivity
|
//! what can pile up) and liveness is covered by the inactivity
|
||||||
//! timeout ([`crate::Limits::inactivity_timeout`], always set) — a
|
//! timeout ([`crate::Limits::inactivity_timeout`], always set); a
|
||||||
//! deadline here would be a behavior change (a slow client may stall
|
//! deadline here would be a behavior change (a slow client may stall
|
||||||
//! its stream, vanilla sshd's posture included), not a coverage
|
//! its stream, vanilla sshd's posture included), not a coverage
|
||||||
//! minor. So the limit is recorded, not "fixed".
|
//! minor. So the limit is recorded, not "fixed".
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
//!
|
//!
|
||||||
//! The requests arrive on the packet loop after the serving task
|
//! The requests arrive on the packet loop after the serving task
|
||||||
//! owns the wire, and reach the spawned bridge through the
|
//! owns the wire, and reach the spawned bridge through the
|
||||||
//! **per-channel control surface** ([`BridgeSurface`] — handler
|
//! **per-channel control surface** ([`BridgeSurface`], handler
|
||||||
//! state while the row serves): a bounded mpsc whose send half
|
//! state while the row serves): a bounded mpsc whose send half
|
||||||
//! forward_control `try_send`s the control chunk (never awaited on
|
//! forward_control `try_send`s the control chunk (never awaited on
|
||||||
//! the packet loop). `window_change_request` →
|
//! the packet loop). `window_change_request` →
|
||||||
@@ -124,23 +125,23 @@
|
|||||||
//! (alktty's uppercase-name vocabulary). Decisions, recorded:
|
//! (alktty's uppercase-name vocabulary). Decisions, recorded:
|
||||||
//! the SSH control rows are fire-and-forget (no reply exists on
|
//! the SSH control rows are fire-and-forget (no reply exists on
|
||||||
//! the wire), so **a full or closed surface drops the control
|
//! the wire), so **a full or closed surface drops the control
|
||||||
//! event** — the bound keeps a stalled bridge from stalling the
|
//! event**; the bound keeps a stalled bridge from stalling the
|
||||||
//! packet loop (bounded resources, convention 9); **signal names
|
//! packet loop (bounded resources, convention 9); **signal names
|
||||||
//! are accepted verbatim** — no alkssh-side name filter: the
|
//! are accepted verbatim**, no alkssh-side name filter: the
|
||||||
//! backend owns the rejection semantics (alktty passes the name to
|
//! backend owns the rejection semantics (alktty passes the name to
|
||||||
//! the backend's control handle, whose semantics decide), and the
|
//! the backend's control handle, whose semantics decide), and the
|
||||||
//! pipe-mode pid-only vs PTY process-group *scope* is backend
|
//! pipe-mode pid-only vs PTY process-group *scope* is backend
|
||||||
//! semantics (the mapping row note), so there is **no decline
|
//! semantics (the mapping row note), so there is **no decline
|
||||||
//! shape** for a name — nothing on the wire can answer. A resize
|
//! shape** for a name; nothing on the wire can answer. A resize
|
||||||
//! arriving for a channel whose pty is stored but not yet serving
|
//! arriving for a channel whose pty is stored but not yet serving
|
||||||
//! updates the stored params instead (the final size wins —
|
//! updates the stored params instead (the final size wins,
|
||||||
//! vanilla sshd's own ordering); a resize with neither a serving
|
//! vanilla sshd's own ordering); a resize with neither a serving
|
||||||
//! bridge nor a stored pty is dropped.
|
//! bridge nor a stored pty is dropped.
|
||||||
//!
|
//!
|
||||||
//! ## The exit derivation contract (mapping.md §Exit chunk, rules 1–6)
|
//! ## The exit derivation contract (mapping.md §Exit chunk, rules 1–6)
|
||||||
//!
|
//!
|
||||||
//! Normative text and the whole derivation machinery now live in
|
//! Normative text and the whole derivation machinery now live in
|
||||||
//! [`crate::exit_code`] — the shared exit-currency module consumed by
|
//! [`crate::exit_code`], the shared exit-currency module consumed by
|
||||||
//! *both* directions (`w4-op-rows` extracted it out of this module:
|
//! *both* directions (`w4-op-rows` extracted it out of this module:
|
||||||
//! the client rows consume wire exit reports back into the family's
|
//! the client rows consume wire exit reports back into the family's
|
||||||
//! `i32` exit-code convention, the exact inverse arm of the same
|
//! `i32` exit-code convention, the exact inverse arm of the same
|
||||||
@@ -156,11 +157,11 @@
|
|||||||
//!
|
//!
|
||||||
//! The serving task watches two signals: the **per-channel cancel**
|
//! The serving task watches two signals: the **per-channel cancel**
|
||||||
//! (fired by `channel_close`, or surfaced by the surface's drop) and
|
//! (fired by `channel_close`, or surfaced by the surface's drop) and
|
||||||
//! the **connection teardown** ([`Teardown`] — fired by the
|
//! the **connection teardown** ([`Teardown`], fired by the
|
||||||
//! handler's `Drop`; the russh session ended, however it ended). On
|
//! handler's `Drop`; the russh session ended, however it ended). On
|
||||||
//! either, the task aborts the alktty session task — dropping its
|
//! either, the task aborts the alktty session task, dropping its
|
||||||
//! not-yet-resolved `exit_code` future, which is alktty's
|
//! not-yet-resolved `exit_code` future (alktty's
|
||||||
//! kill-on-drop contract firing (its ADR-056) — then closes the SSH
|
//! kill-on-drop contract firing, its ADR-056), then closes the SSH
|
||||||
//! channel and ends. Nothing is fabricated on a cancel. The
|
//! channel and ends. Nothing is fabricated on a cancel. The
|
||||||
//! detached stdin/framing tasks are bounded by their duplex/mpsc/
|
//! detached stdin/framing tasks are bounded by their duplex/mpsc/
|
||||||
//! russh-window bounds and end on the same signals' effects.
|
//! russh-window bounds and end on the same signals' effects.
|
||||||
@@ -182,7 +183,7 @@ use crate::config::SessionSeam;
|
|||||||
use crate::exit_code::{ExitReport, derive_exit, sig_from_name};
|
use crate::exit_code::{ExitReport, derive_exit, sig_from_name};
|
||||||
use crate::observe;
|
use crate::observe;
|
||||||
|
|
||||||
/// The negotiation frame the bridge speaks locally — every locally
|
/// The negotiation frame the bridge speaks locally; every locally
|
||||||
/// constructed request reads `"raw"` (alktty's v1 carriage).
|
/// constructed request reads `"raw"` (alktty's v1 carriage).
|
||||||
pub(crate) const CARRIAGE: &str = "raw";
|
pub(crate) const CARRIAGE: &str = "raw";
|
||||||
|
|
||||||
@@ -192,11 +193,11 @@ pub(crate) const CARRIAGE: &str = "raw";
|
|||||||
/// (§the control rows). The shim's scripted rows exercise a handful.
|
/// (§the control rows). The shim's scripted rows exercise a handful.
|
||||||
pub(crate) const CONTROL_SURFACE_BOUND: usize = 64;
|
pub(crate) const CONTROL_SURFACE_BOUND: usize = 64;
|
||||||
|
|
||||||
/// The translation bridge duplex capacity — the wave-2 loopback
|
/// The translation bridge duplex capacity, the wave-2 loopback
|
||||||
/// harness's per-direction convention, kept for the bridge pairs.
|
/// harness's per-direction convention, kept for the bridge pairs.
|
||||||
const BRIDGE_DUPLEX_CAPACITY: usize = 65_536;
|
const BRIDGE_DUPLEX_CAPACITY: usize = 65_536;
|
||||||
|
|
||||||
/// The stdin pump's read buffer — the SSH-side chunk granularity
|
/// The stdin pump's read buffer, the SSH-side chunk granularity
|
||||||
/// (per-chunk payloads are bounded in practice by russh's
|
/// (per-chunk payloads are bounded in practice by russh's
|
||||||
/// `maximum_packet_size`, here by [`MAX_CHUNK_LEN`] on the frame).
|
/// `maximum_packet_size`, here by [`MAX_CHUNK_LEN`] on the frame).
|
||||||
const STDIN_PUMP_BUF: usize = 16 * 1024;
|
const STDIN_PUMP_BUF: usize = 16 * 1024;
|
||||||
@@ -210,7 +211,7 @@ pub(crate) fn clamp_u16(value: u32) -> u16 {
|
|||||||
|
|
||||||
/// The `pty_request` args → `TerminalParamsWire` conversion (§the
|
/// The `pty_request` args → `TerminalParamsWire` conversion (§the
|
||||||
/// negotiation section): SSH ttymodes → the **reserved modes JSON
|
/// negotiation section): SSH ttymodes → the **reserved modes JSON
|
||||||
/// `{}`** (alktty OQ-44's reservation — the map is honest: nothing
|
/// `{}`** (alktty OQ-44's reservation; the map is honest: nothing
|
||||||
/// is carried, exactly what the reservation asks).
|
/// is carried, exactly what the reservation asks).
|
||||||
pub fn terminal_params(
|
pub fn terminal_params(
|
||||||
term: &str,
|
term: &str,
|
||||||
@@ -251,15 +252,15 @@ pub(crate) fn negotiate_request(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One parsed bridge-side frame from alktty's `client_send` half —
|
/// One parsed bridge-side frame from alktty's `client_send` half:
|
||||||
/// the 4-byte first-window disambiguation's outcome (§the
|
/// the 4-byte first-window disambiguation's outcome (§the
|
||||||
/// translation bridge).
|
/// translation bridge).
|
||||||
pub(crate) enum BridgeFrame {
|
pub(crate) enum BridgeFrame {
|
||||||
/// A raw alktty chunk (data or server→client control).
|
/// A raw alktty chunk (data or server→client control).
|
||||||
Chunk(Chunk),
|
Chunk(Chunk),
|
||||||
/// The length-prefixed negotiation error frame — the session
|
/// The length-prefixed negotiation error frame: the session
|
||||||
/// failed validation/allocation/ownership at the seam. The bytes
|
/// failed validation/allocation/ownership at the seam. The bytes
|
||||||
/// are alktty-side detail (never read here — only the
|
/// are alktty-side detail (never read here, only the
|
||||||
/// vocabulary value rides the decline event).
|
/// vocabulary value rides the decline event).
|
||||||
NegotiationError,
|
NegotiationError,
|
||||||
/// The half ended (alktty's task completed or was cancelled)
|
/// The half ended (alktty's task completed or was cancelled)
|
||||||
@@ -269,10 +270,10 @@ pub(crate) enum BridgeFrame {
|
|||||||
Break,
|
Break,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads one bridge frame — the disambiguation invariant: the first
|
/// Reads one bridge frame (the disambiguation invariant: the first
|
||||||
/// byte `0x00` is a negotiation error frame's length-prefix high
|
/// byte `0x00` is a negotiation error frame's length-prefix high
|
||||||
/// byte; anything ≤ 4 is a chunk `stream_type`. Bounds-checked
|
/// byte; anything ≤ 4 is a chunk `stream_type`). Bounds-checked
|
||||||
/// against [`MAX_CHUNK_LEN`] both directions — a rogue length never
|
/// against [`MAX_CHUNK_LEN`] both directions, so a rogue length never
|
||||||
/// triggers an oversized read.
|
/// triggers an oversized read.
|
||||||
pub(crate) async fn read_bridge_frame(
|
pub(crate) async fn read_bridge_frame(
|
||||||
reader: &mut (impl tokio::io::AsyncRead + Unpin),
|
reader: &mut (impl tokio::io::AsyncRead + Unpin),
|
||||||
@@ -318,8 +319,8 @@ pub(crate) async fn read_bridge_frame(
|
|||||||
|
|
||||||
/// The exit chunk's JSON payload → the derived code
|
/// The exit chunk's JSON payload → the derived code
|
||||||
/// (`{"type":"exit","code":N}`). Anything else on `STREAM_CTRL_OUT`
|
/// (`{"type":"exit","code":N}`). Anything else on `STREAM_CTRL_OUT`
|
||||||
/// has no contract meaning — and an exit chunk that cannot parse has
|
/// has no contract meaning, and an exit chunk that cannot parse has
|
||||||
/// no fidelity input — both map to `None` (no report; the plain
|
/// no fidelity input; both map to `None` (no report; the plain
|
||||||
/// close posture).
|
/// close posture).
|
||||||
fn exit_chunk_code(payload: &[u8]) -> Option<i32> {
|
fn exit_chunk_code(payload: &[u8]) -> Option<i32> {
|
||||||
ControlMessage::from_slice(payload)
|
ControlMessage::from_slice(payload)
|
||||||
@@ -330,7 +331,7 @@ fn exit_chunk_code(payload: &[u8]) -> Option<i32> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The per-channel bridge surface — handler state while a session row
|
/// The per-channel bridge surface: handler state while a session row
|
||||||
/// serves (§the control rows). The handler's `window_change_request`
|
/// serves (§the control rows). The handler's `window_change_request`
|
||||||
/// / `signal` hooks reach the spawned bridge **through this struct**
|
/// / `signal` hooks reach the spawned bridge **through this struct**
|
||||||
/// (the requests arrive on the packet loop after the serving task
|
/// (the requests arrive on the packet loop after the serving task
|
||||||
@@ -338,17 +339,17 @@ fn exit_chunk_code(payload: &[u8]) -> Option<i32> {
|
|||||||
/// per-channel cancel signal `channel_close` fires. Packet-loop only
|
/// per-channel cancel signal `channel_close` fires. Packet-loop only
|
||||||
/// (single-writer; no lock).
|
/// (single-writer; no lock).
|
||||||
pub(crate) struct BridgeSurface {
|
pub(crate) struct BridgeSurface {
|
||||||
/// The control-surface send half — the same bounded mpsc the
|
/// The control-surface send half, the same bounded mpsc the
|
||||||
/// serving task's stdin pump feeds.
|
/// serving task's stdin pump feeds.
|
||||||
pub(crate) sender: mpsc::Sender<Chunk>,
|
pub(crate) sender: mpsc::Sender<Chunk>,
|
||||||
/// The per-channel cancel signal — `channel_close` fires it. The
|
/// The per-channel cancel signal (`channel_close` fires it). The
|
||||||
/// serving task interprets the fire *or* the half's drop as the
|
/// serving task interprets the fire *or* the half's drop as the
|
||||||
/// cancel.
|
/// cancel.
|
||||||
pub(crate) cancel: watch::Sender<bool>,
|
pub(crate) cancel: watch::Sender<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BridgeSurface {
|
impl BridgeSurface {
|
||||||
/// Forwards one control row into the bridge — `try_send`, never
|
/// Forwards one control row into the bridge via `try_send`, never
|
||||||
/// awaited on the packet loop (§the control rows); a full or
|
/// awaited on the packet loop (§the control rows); a full or
|
||||||
/// closed surface drops the event.
|
/// closed surface drops the event.
|
||||||
pub(crate) fn forward_control(&self, message: &ControlMessage) -> bool {
|
pub(crate) fn forward_control(&self, message: &ControlMessage) -> bool {
|
||||||
@@ -404,12 +405,12 @@ impl Teardown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the SSH half of the channel and feeds stdin chunks — the
|
/// Reads the SSH half of the channel and feeds stdin chunks, the
|
||||||
/// SSH `data` → `Chunk::stdin` adapter (§the translation bridge).
|
/// SSH `data` → `Chunk::stdin` adapter (§the translation bridge).
|
||||||
/// On EOF or a read error it sends **the zero-length stdin-chunk
|
/// On EOF or a read error it sends **the zero-length stdin-chunk
|
||||||
/// sentinel** (the EOF translation decision: identical in effect to
|
/// sentinel** (the EOF translation decision: identical in effect to
|
||||||
/// alktty's `ControlMessage::Eof` — the backend's stdin shuts while
|
/// alktty's `ControlMessage::Eof`, with the backend's stdin shut
|
||||||
/// the session keeps pumping) and ends.
|
/// while the session keeps pumping) and ends.
|
||||||
async fn stdin_pump(
|
async fn stdin_pump(
|
||||||
ssh_read: impl tokio::io::AsyncRead + Unpin + 'static,
|
ssh_read: impl tokio::io::AsyncRead + Unpin + 'static,
|
||||||
chunks: mpsc::Sender<Chunk>,
|
chunks: mpsc::Sender<Chunk>,
|
||||||
@@ -450,9 +451,9 @@ async fn stdin_pump(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Consumes the framed-input mpsc and writes alktty chunks into the
|
/// Consumes the framed-input mpsc and writes alktty chunks into the
|
||||||
/// session's `client_recv` duplex half — the framing task. Ends after
|
/// session's `client_recv` duplex half, the framing task. Ends after
|
||||||
/// the EOF sentinel is written (nothing rides the data path past
|
/// the EOF sentinel is written (nothing rides the data path past
|
||||||
/// SSH EOF), on a mpsc close (every sender gone — the cancelled or
|
/// SSH EOF), on a mpsc close (every sender gone, the cancelled or
|
||||||
/// completed session), or on a framing failure.
|
/// completed session), or on a framing failure.
|
||||||
async fn framing_task(mut chunks: mpsc::Receiver<Chunk>, writer: tokio::io::DuplexStream) {
|
async fn framing_task(mut chunks: mpsc::Receiver<Chunk>, writer: tokio::io::DuplexStream) {
|
||||||
let mut writer = ChunkWriter::new(writer);
|
let mut writer = ChunkWriter::new(writer);
|
||||||
@@ -468,7 +469,7 @@ async fn framing_task(mut chunks: mpsc::Receiver<Chunk>, writer: tokio::io::Dupl
|
|||||||
|
|
||||||
/// The exit report + close + lifecycle event, in the ADR-006
|
/// The exit report + close + lifecycle event, in the ADR-006
|
||||||
/// decision-4 order: **status before close, from the spawned task**.
|
/// decision-4 order: **status before close, from the spawned task**.
|
||||||
/// A failed send means the session is gone — the lifecycle's one
|
/// A failed send means the session is gone; the lifecycle's one
|
||||||
/// `warn` records it (the report that could not land).
|
/// `warn` records it (the report that could not land).
|
||||||
async fn report_exit(
|
async fn report_exit(
|
||||||
handle: &Handle,
|
handle: &Handle,
|
||||||
@@ -501,7 +502,7 @@ async fn report_exit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fails the request and closes the channel from the serving path —
|
/// Fails the request and closes the channel from the serving path,
|
||||||
/// the serving-path decline shape ([`crate::serve`]'s mechanics) with
|
/// the serving-path decline shape ([`crate::serve`]'s mechanics) with
|
||||||
/// the lifecycle events and the given reason. Used for `no-identity`.
|
/// the lifecycle events and the given reason. Used for `no-identity`.
|
||||||
async fn fail_and_close(
|
async fn fail_and_close(
|
||||||
@@ -528,7 +529,7 @@ async fn fail_and_close(
|
|||||||
/// [`alktty::drive_session_pre_negotiated`]; the alktty session task
|
/// [`alktty::drive_session_pre_negotiated`]; the alktty session task
|
||||||
/// and the SSH→chunks pump are spawned here, and *this* future drives
|
/// and the SSH→chunks pump are spawned here, and *this* future drives
|
||||||
/// the chunks→SSH direction inline so the exit report is the path's
|
/// the chunks→SSH direction inline so the exit report is the path's
|
||||||
/// own final act (status before close — ADR-006 decision 4). The
|
/// own final act (status before close, per ADR-006 decision 4). The
|
||||||
/// channel was taken by the dispatch; the spawned task owns the
|
/// channel was taken by the dispatch; the spawned task owns the
|
||||||
/// stream and its report surface.
|
/// stream and its report surface.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -688,14 +689,14 @@ async fn drive_chunks_to_ssh(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
/// - *Contract stamp:* the no-exit-chunk close family's frame
|
/// - *Contract stamp:* the no-exit-chunk close family's frame
|
||||||
/// classification (`w4-tty-coverage` — the wire row in
|
/// classification (`w4-tty-coverage`; the wire row in
|
||||||
/// `tests/tty_mapping.rs` lands the family's serving outcome): a
|
/// `tests/tty_mapping.rs` lands the family's serving outcome): a
|
||||||
/// clean read-end classifies `Closed`, an oversized claimed
|
/// clean read-end classifies `Closed`, an oversized claimed
|
||||||
/// length classifies `Break` — bounds-checked both directions,
|
/// length classifies `Break` (bounds-checked both directions,
|
||||||
/// never an oversized read. `Break`'s serving arm is one body
|
/// never an oversized read). `Break`'s serving arm is one body
|
||||||
/// with `Closed`'s; through alktty's own validated `ChunkWriter`
|
/// with `Closed`'s; through alktty's own validated `ChunkWriter`
|
||||||
/// a `Break` frame is unreachable on the duplex (the drainer
|
/// a `Break` frame is unreachable on the duplex (the drainer
|
||||||
/// rejects the oversized chunk locally and the read sees EOF —
|
/// rejects the oversized chunk locally and the read sees EOF,
|
||||||
/// the wire row's construction), so the classification rows pin
|
/// the wire row's construction), so the classification rows pin
|
||||||
/// the defensive arm at the frame layer.
|
/// the defensive arm at the frame layer.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
+196
-93
@@ -1,21 +1,23 @@
|
|||||||
//! The `direct-tcpip` / `direct-streamlocal` mapping — the tunnel
|
//! The `direct-tcpip` / `direct-streamlocal` mapping, the tunnel
|
||||||
//! branch's route-1 bridge (`w3-tunnel-direct`, ADR-006 decision 1):
|
//! branch's route-1 bridge (`w3-tunnel-direct`, ADR-006 decision 1):
|
||||||
//! the adapter-side bridge POC-3 stage C proved, re-landed as contract
|
//! the adapter-side bridge POC-3 stage C proved, re-landed as contract
|
||||||
//! against published alktunnels 0.4.1.
|
//! against published alktunnels 0.4.1, with the forwarded serving
|
||||||
|
//! surface adopted to alktunnels 0.5.0's establisher shape (the
|
||||||
|
//! review-003 W2 adoption, the `ForwardedGate` correlation gate).
|
||||||
//!
|
//!
|
||||||
//! ## The route-1 bridge (ADR-006 decision 1)
|
//! ## The route-1 bridge (ADR-006 decision 1)
|
||||||
//!
|
//!
|
||||||
//! alkssh holds the assembly-supplied `Arc<ChannelClient>` into the
|
//! alkssh holds the assembly-supplied `Arc<ChannelClient>` into the
|
||||||
//! family fabric (the serve-config's `channel_client` — the wave-1
|
//! family fabric (the serve-config's `channel_client`, the wave-1
|
||||||
//! config field's consuming code). Per accepted `direct-tcpip` /
|
//! config field's consuming code). Per accepted `direct-tcpip` /
|
||||||
//! `direct-streamlocal` open, the target/substrate are constructed
|
//! `direct-streamlocal` open, the target/substrate are constructed
|
||||||
//! from the SSH open arguments (`direct-tcpip`(host, port) →
|
//! from the SSH open arguments (`direct-tcpip`(host, port) →
|
||||||
//! `SubstrateAddr::HostPort` + `Substrate::Tcp`;
|
//! `SubstrateAddr::HostPort` + `Substrate::Tcp`;
|
||||||
//! `direct-streamlocal`(socket path) → `SubstrateAddr::Path` +
|
//! `direct-streamlocal`(socket path) → `SubstrateAddr::Path` +
|
||||||
//! `Substrate::Unix` — the mapping.md row's treatment) and the flow is
|
//! `Substrate::Unix`, the mapping.md row's treatment) and the flow is
|
||||||
//! exactly the POC recipe: **`TunnelSession::open_direct(&client,
|
//! exactly the POC recipe: **`TunnelSession::open_direct(&client,
|
||||||
//! target, substrate)` → accept only after the fabric open succeeds**
|
//! target, substrate)` → accept only after the fabric open succeeds**
|
||||||
//! (`ChannelOpenHandle::accept` consumes the handle — the reject shape
|
//! (`ChannelOpenHandle::accept` consumes the handle; the reject shape
|
||||||
//! is only possible pre-accept, which is what makes accept-after-open
|
//! is only possible pre-accept, which is what makes accept-after-open
|
||||||
//! the only honest flow) → on success `take_halves()` pumped against
|
//! the only honest flow) → on success `take_halves()` pumped against
|
||||||
//! the taken channel's stream ([`alkcall::channels::pump::pump_bidi`],
|
//! the taken channel's stream ([`alkcall::channels::pump::pump_bidi`],
|
||||||
@@ -23,57 +25,58 @@
|
|||||||
//! the spawn-and-return constraint. The open hook returns fast with
|
//! the spawn-and-return constraint. The open hook returns fast with
|
||||||
//! the fabric open in flight.
|
//! the fabric open in flight.
|
||||||
//!
|
//!
|
||||||
//! **Fabric failure → the explicit `ConnectFailed` reject** — the
|
//! **Fabric failure → the explicit `ConnectFailed` reject**; the
|
||||||
//! wave-2 seam stub's reason surviving as the real decline
|
//! wave-2 seam stub's reason surviving as the real decline
|
||||||
//! ([`crate::dispatch::connect_failed_open_decline`]; the stub itself
|
//! ([`crate::dispatch::connect_failed_open_decline`]; the stub itself
|
||||||
//! is retired). The channel is never established, so the SSH client
|
//! is retired). The channel is never established, so the SSH client
|
||||||
//! observes a clean, channel-scoped open refusal (POC-1 §5 posture)
|
//! observes a clean, channel-scoped open refusal (POC-1 §5 posture)
|
||||||
//! — vanilla clients retry-observe a down forward instead of a hang.
|
//! : vanilla clients retry-observe a down forward instead of a hang.
|
||||||
//!
|
//!
|
||||||
//! ## The `-R` listen flow (`w3-tunnel-listen` — the fabric listens,
|
//! ## The `-R` listen flow (`w3-tunnel-listen`: the fabric listens,
|
||||||
//! the client dials)
|
//! the client dials)
|
||||||
//!
|
//!
|
||||||
//! The server direction of the route-1 bridge (`mapping.md`
|
//! The server direction of the route-1 bridge (`mapping.md`
|
||||||
//! §Channel-type rows, the `forwarded-tcpip` row + §Global-request
|
//! §Channel-type rows, the `forwarded-tcpip` row + §Global-request
|
||||||
//! rows). Per the family door semantics: the door owns nothing local
|
//! rows). Per the family door semantics: the door owns nothing local,
|
||||||
//! — **the fabric listens**; per arrival the data flows back toward
|
//! **the fabric listens**; per arrival the data flows back toward
|
||||||
//! the vanilla SSH client, which dials its own target. Three moving
|
//! the vanilla SSH client, which dials its own target. Three moving
|
||||||
//! parts, all decided against alktunnels 0.4.1's actual surfaces with
|
//! parts, all decided against alktunnels 0.4.1's actual surfaces with
|
||||||
//! POC-3 §C.4 as the template:
|
//! POC-3 §C.4 as the template (the gate adoption rides 0.5.0's
|
||||||
|
//! establisher variant, above):
|
||||||
//!
|
//!
|
||||||
//! 1. **The listen dial** (`tcpip_forward` / `streamlocal_forward`):
|
//! 1. **The listen dial** (`tcpip_forward` / `streamlocal_forward`):
|
||||||
//! the door maps the vanilla listen request family-ward as
|
//! the door maps the vanilla listen request family-ward as
|
||||||
//! `TunnelParams { resource: <the wire bind endpoint string>,
|
//! `TunnelParams { resource: <the wire bind endpoint string>,
|
||||||
//! substrate: Tcp/Unix }` — the **resource-naming convention
|
//! substrate: Tcp/Unix }`; the **resource-naming convention
|
||||||
//! recorded** (the one open surface this flow's composition had):
|
//! recorded** (the one open surface this flow's composition had):
|
||||||
//! ADR-001 keeps the wire field a *resource identifier*, and a
|
//! ADR-001 keeps the wire field a *resource identifier*, and a
|
||||||
//! fabric serving vanilla `-R` opt into dynamic-address binding by
|
//! fabric serving vanilla `-R` opt into dynamic-address binding by
|
||||||
//! its own establisher/bind-closure treating the mapped string as
|
//! its own establisher/bind-closure treating the mapped string as
|
||||||
//! its bind endpoint (`bindfirst_establisher` +
|
//! its bind endpoint (`bindfirst_establisher` +
|
||||||
//! `register_openable_with_establisher`, the alkcall ADR-049
|
//! `register_openable_with_establisher`, the alkcall ADR-049
|
||||||
//! establisher shape — who may bind what is the fabric's ACL +
|
//! establisher shape: who may bind what is the fabric's ACL +
|
||||||
//! its bind closure, assembly policy at the family's provider,
|
//! its bind closure, assembly policy at the family's provider,
|
||||||
//! never the door's). The dial is the published consumer surface —
|
//! never the door's). The dial is the published consumer surface,
|
||||||
//! `TunnelListener::open(client, params)` on the held
|
//! `TunnelListener::open(client, params)` on the held
|
||||||
//! `ChannelClient` — bounded by [`LISTEN_DIAL_TIMEOUT`]; the
|
//! `ChannelClient`, bounded by [`LISTEN_DIAL_TIMEOUT`]; the
|
||||||
//! reply's `bound` + minted `listen` token (the alkcall ADR-049
|
//! reply's `bound` + minted `listen` token (the alkcall ADR-049
|
||||||
//! amendment-3 establisher fields) are read through
|
//! amendment-3 establisher fields) are read through
|
||||||
//! `parse-listen-reply`-class accessors; **a reply without both is
|
//! `parse-listen-reply`-class accessors; **a reply without both is
|
||||||
//! a declined listen with the fabric listener torn down
|
//! a declined listen with the fabric listener torn down
|
||||||
//! immediately** (never a minted-but-uncorrelatable listen — a
|
//! immediately** (never a minted-but-uncorrelatable listen; a
|
||||||
//! declined listen never left a half state).
|
//! declined listen never left a half state).
|
||||||
//! 2. **The door-side arrival surface** — the composition decision
|
//! 2. **The door-side arrival surface**, the composition decision
|
||||||
//! (the task's second `w3-seam-config` course correction): the
|
//! (the task's second `w3-seam-config` course correction): the
|
||||||
//! fabric dials `tunnel/forwarded` *toward the door* per alktunnels'
|
//! fabric dials `tunnel/forwarded` *toward the door* per alktunnels'
|
||||||
//! bind-first dispatch (`make_bindfirst_accept_handler` +
|
//! bind-first dispatch (`make_bindfirst_accept_handler` +
|
||||||
//! `ForwardedOpener` — the producer calls the op on the channel-0
|
//! `ForwardedOpener`): the producer calls the op on the channel-0
|
||||||
//! call plane toward the listen opener, and the door reply carries
|
//! call plane toward the listen opener, and the door reply carries
|
||||||
//! the door-allocated channel id the producer adopts; alkcall's
|
//! the door-allocated channel id the producer adopts; alkcall's
|
||||||
//! odd/even split keeps the two open directions collision-free).
|
//! odd/even split keeps the two open directions collision-free).
|
||||||
//! Serving that op needs the door's channel-0 serving loop — an
|
//! Serving that op needs the door's channel-0 serving loop, an
|
||||||
//! alkcall `ChannelClient` built with
|
//! alkcall `ChannelClient` built with
|
||||||
//! `from_connection_with_serving`, whose registry the assembly owns
|
//! `from_connection_with_serving`, whose registry the assembly owns,
|
||||||
//! — so the serve-config carries [`crate::config::TunnelListenSeam`]
|
//! so the serve-config carries [`crate::config::TunnelListenSeam`]
|
||||||
//! (the same `Arc<OperationRegistry>` the assembly wired); alkssh
|
//! (the same `Arc<OperationRegistry>` the assembly wired); alkssh
|
||||||
//! registers the forwarded openable itself in
|
//! registers the forwarded openable itself in
|
||||||
//! [`install_forwarded_surface`] (called from
|
//! [`install_forwarded_surface`] (called from
|
||||||
@@ -82,25 +85,34 @@
|
|||||||
//! client's manager + alktunnels' `tunnel_forwarded_spec()` +
|
//! client's manager + alktunnels' `tunnel_forwarded_spec()` +
|
||||||
//! `make_forwarded_op_handler` over the crate's arrival pump, the
|
//! `make_forwarded_op_handler` over the crate's arrival pump, the
|
||||||
//! wire-level ACL (`tunnel:forwarded` scope) and the per-connection
|
//! wire-level ACL (`tunnel:forwarded` scope) and the per-connection
|
||||||
//! channel budget enforced inside alkcall's dispatch.
|
//! channel budget enforced inside alkcall's dispatch, registered
|
||||||
//! 3. **The arrival pump** — the `forwarded-tcpip` data leg: the
|
//! in the **establisher variant** (alktunnels 0.5.0's
|
||||||
|
//! `register_tunnel_forwarded_openable_with_establisher`, the
|
||||||
|
//! review-003 W2 adoption): the router's correlation lookup rides
|
||||||
|
//! the establisher's `ForwardedGate`, so a stale/missing token
|
||||||
|
//! fails the open typed at the call plane (`channel:open_failed`,
|
||||||
|
//! `unknown_resource`) before any channel is born; the handler's
|
||||||
|
//! re-lookup keeps the residual-race +
|
||||||
|
//! cap-admission teardown-at-birth fallback.
|
||||||
|
//! 3. **The arrival pump**, the `forwarded-tcpip` data leg: the
|
||||||
//! registered [`ForwardedHandler`]-closure correlates the
|
//! registered [`ForwardedHandler`]-closure correlates the
|
||||||
//! arrival's `listen` token through [`ListenRouter`] (the route was
|
//! arrival's `listen` token through [`ListenRouter`] (the route was
|
||||||
//! registered at listen acceptance with the owning session's
|
//! registered at listen acceptance with the owning session's
|
||||||
//! handle + cap); a miss is **teardown-at-birth** (drop the fabric
|
//! handle + cap); a miss here is the gate-pass residual race,
|
||||||
|
//! still **teardown-at-birth** (drop the fabric
|
||||||
//! half, never a half-open SSH channel). On a hit, per vanilla `-R`
|
//! half, never a half-open SSH channel). On a hit, per vanilla `-R`
|
||||||
//! convention: the door opens a `forwarded-tcpip` channel *toward
|
//! convention: the door opens a `forwarded-tcpip` channel *toward
|
||||||
//! the client* echoing **the listen request's own args** — the
|
//! the client* echoing **the listen request's own args**, the
|
||||||
//! connected address as the wire `tcpip_forward` request carried it
|
//! connected address as the wire `tcpip_forward` request carried it
|
||||||
//! verbatim (the OpenSSH client matches that exact string; for
|
//! verbatim (the OpenSSH client matches that exact string; for
|
||||||
//! `port = 0` mints the *actually bound* port from the fabric's
|
//! `port = 0` mints the *actually bound* port from the fabric's
|
||||||
//! `bound` reply — the sshd parity recorded in
|
//! `bound` reply, the sshd parity recorded in
|
||||||
//! [`ListenRoute`]) — with the `TunnelForwardedParams.peer` arrival
|
//! [`ListenRoute`]), with the `TunnelForwardedParams.peer` arrival
|
||||||
//! mapped to the originator fields (`HostPort` → address/port;
|
//! mapped to the originator fields (`HostPort` → address/port;
|
||||||
//! `Path` → path + port 0 — the degenerate but typed mapping,
|
//! `Path` → path + port 0, the degenerate but typed mapping,
|
||||||
//! unreachable for a TCP-minted listen). The accepted-or-rejected
|
//! unreachable for a TCP-minted listen). The accepted-or-rejected
|
||||||
//! decision is the in-crate cap (the wave-2 gate's booked acceptor
|
//! decision is the in-crate cap (the wave-2 gate's booked acceptor
|
||||||
//! vocabulary — the per-arrival admission; a client-side rejection
|
//! vocabulary, the per-arrival admission; a client-side rejection
|
||||||
//! or a dead session releases the count, the established channel's
|
//! or a dead session releases the count, the established channel's
|
||||||
//! release rides [`crate::handler`]'s `channel_close` like every
|
//! release rides [`crate::handler`]'s `channel_close` like every
|
||||||
//! established channel). Accept → the fabric `BiStream` pumped
|
//! established channel). Accept → the fabric `BiStream` pumped
|
||||||
@@ -113,7 +125,7 @@
|
|||||||
//!
|
//!
|
||||||
//! **The inline listen dial (recorded decision):** the
|
//! **The inline listen dial (recorded decision):** the
|
||||||
//! spawn-and-return rule keeps *serving paths* out of the packet
|
//! spawn-and-return rule keeps *serving paths* out of the packet
|
||||||
//! loop — but the `tcpip_forward` global request's reply is bound to
|
//! loop, but the `tcpip_forward` global request's reply is bound to
|
||||||
//! the hook itself (russh answers the want-reply from the hook's
|
//! the hook itself (russh answers the want-reply from the hook's
|
||||||
//! return + the `&mut u32` out-param; no handle surface responds to a
|
//! return + the `&mut u32` out-param; no handle surface responds to a
|
||||||
//! global request from a spawned task at this pin), so the listen
|
//! global request from a spawned task at this pin), so the listen
|
||||||
@@ -124,18 +136,18 @@
|
|||||||
//! packet loop), and its stall is bounded: other channels of the
|
//! packet loop), and its stall is bounded: other channels of the
|
||||||
//! connection pause only for [`LISTEN_DIAL_TIMEOUT`], and the
|
//! connection pause only for [`LISTEN_DIAL_TIMEOUT`], and the
|
||||||
//! fabric-dial failure answers a denied listen. All serving below the
|
//! fabric-dial failure answers a denied listen. All serving below the
|
||||||
//! reply (none — the listen returns) and all arrival paths stay
|
//! reply (none, since the listen returns) and all arrival paths stay
|
||||||
//! spawned.
|
//! spawned.
|
||||||
//!
|
//!
|
||||||
//! ## Bounded bookkeeping (AGENTS.md convention 9)
|
//! ## Bounded bookkeeping (AGENTS.md convention 9)
|
||||||
//!
|
//!
|
||||||
//! - **The per-session listener map** (the session-handler's
|
//! - **The per-session listener map** (the session-handler's
|
||||||
//! `listeners` — packet-loop-only writer, like the `channels`/
|
//! `listeners`, packet-loop-only writer, like the `channels`/
|
||||||
//! `ptys` stores) maps the minted token → the pending listen (wire
|
//! `ptys` stores) maps the minted token → the pending listen (wire
|
||||||
//! args, bound port, the [`alktunnels::consumer::TunnelListener`]
|
//! args, bound port, the [`alktunnels::consumer::TunnelListener`]
|
||||||
//! whose `close`/`Drop` perform the fabric teardown). Its bound:
|
//! whose `close`/`Drop` perform the fabric teardown). Its bound:
|
||||||
//! the listen count shares the `Limits::max_concurrent_channels`
|
//! the listen count shares the `Limits::max_concurrent_channels`
|
||||||
//! *ceiling* in a dedicated counter — a listen is at least one
|
//! *ceiling* in a dedicated counter; a listen is at least one
|
||||||
//! future channel's potential, sessions minting unbounded listeners
|
//! future channel's potential, sessions minting unbounded listeners
|
||||||
//! would mint an unbounded fabric-channel map on the shared leg.
|
//! would mint an unbounded fabric-channel map on the shared leg.
|
||||||
//! - **The route map** ([`ListenRouter`], shared with the registered
|
//! - **The route map** ([`ListenRouter`], shared with the registered
|
||||||
@@ -143,29 +155,29 @@
|
|||||||
//! bounded by construction: entries are minted exactly at accepted
|
//! bounded by construction: entries are minted exactly at accepted
|
||||||
//! listens (listener-cap gated), removed 1:1 at the cancel forms
|
//! listens (listener-cap gated), removed 1:1 at the cancel forms
|
||||||
//! and at the owning session's teardown (the handler's `Drop`
|
//! and at the owning session's teardown (the handler's `Drop`
|
||||||
//! removes its tokens — a stale route to a dead handle only ever
|
//! removes its tokens; a stale route to a dead handle only ever
|
||||||
//! costs one failed open that returns teardown-at-birth, never a
|
//! costs one failed open that returns teardown-at-birth, never a
|
||||||
//! half-open SSH channel).
|
//! half-open SSH channel).
|
||||||
//! - **A cancelled / torn-down listen never half-serves**: the cancel
|
//! - **A cancelled / torn-down listen never half-serves**: the cancel
|
||||||
//! path removes the route before any teardown (a racing arrival
|
//! path removes the route before any teardown (a racing arrival
|
||||||
//! misses → teardown-at-birth), and `cancel_*` of an unknown
|
//! misses → teardown-at-birth), and `cancel_*` of an unknown
|
||||||
//! (address, bound-port) pair answers `false` — the vanilla
|
//! (address, bound-port) pair answers `false`, the vanilla
|
||||||
//! cancel contract (sshd parity: match by the raw wire string +
|
//! cancel contract (sshd parity: match by the raw wire string +
|
||||||
//! the bound port). One recorded caveat: an arrival that *cloned*
|
//! the bound port). One recorded caveat: an arrival that *cloned*
|
||||||
//! its route just before the cancel removes it still completes its
|
//! its route just before the cancel removes it still completes its
|
||||||
//! open + pump (the forwarded channel is independent of the
|
//! open + pump (the forwarded channel is independent of the
|
||||||
//! listener's teardown — the arrival is never half-open, but a
|
//! listener's teardown; the arrival is never half-open, but a
|
||||||
//! cancelled listen can deliver one already-accepted connection;
|
//! cancelled listen can deliver one already-accepted connection;
|
||||||
//! sshd parity).
|
//! sshd parity).
|
||||||
//!
|
//!
|
||||||
//! ## The runtime-Unix gate (OQ-11)
|
//! ## The runtime-Unix gate (OQ-11)
|
||||||
//!
|
//!
|
||||||
//! `direct-streamlocal` compiles wherever `tunnel` compiles (universal
|
//! `direct-streamlocal` compiles wherever `tunnel` compiles (universal
|
||||||
//! compile — no per-platform feature fork); **serving is runtime-Unix-
|
//! compile, no per-platform feature fork); **serving is runtime-Unix-
|
||||||
//! gated**: off-Unix a clean open-decline **before any fabric call**
|
//! gated**: off-Unix a clean open-decline **before any fabric call**
|
||||||
//! (never a partial bridge), on-Unix the real bridge. The gate lives
|
//! (never a partial bridge), on-Unix the real bridge. The gate lives
|
||||||
//! in the dispatch hook ([`crate::handler`]'s `direct-streamlocal`
|
//! in the dispatch hook ([`crate::handler`]'s `direct-streamlocal`
|
||||||
//! row), before the cap admission — a platform-refused open consumes
|
//! row), before the cap admission; a platform-refused open consumes
|
||||||
//! nothing. The gate rides the `streamlocal_forward` listen row the
|
//! nothing. The gate rides the `streamlocal_forward` listen row the
|
||||||
//! same way (`w3-tunnel-listen`).
|
//! same way (`w3-tunnel-listen`).
|
||||||
//!
|
//!
|
||||||
@@ -173,25 +185,25 @@
|
|||||||
//!
|
//!
|
||||||
//! The resolved SSH identity is the auth bridge's ride-along in
|
//! The resolved SSH identity is the auth bridge's ride-along in
|
||||||
//! handler state (`auth_publickey` stores; the row dispatches never
|
//! handler state (`auth_publickey` stores; the row dispatches never
|
||||||
//! serve anonymously — the `no-identity` guard, unreachable on the
|
//! serve anonymously, the `no-identity` guard, unreachable on the
|
||||||
//! wire because russh authorizes before channels exist, fail-closed
|
//! wire because russh authorizes before channels exist, fail-closed
|
||||||
//! like the git/tty rows' `w2-auth-bridge` parked item discharged
|
//! like the git/tty rows' `w2-auth-bridge` parked item discharged
|
||||||
//! there). The guard gates every tunnel open **at the door**; the
|
//! there). The guard gates every tunnel open **at the door**; the
|
||||||
//! open that proceeds then rides the channels connection into the
|
//! open that proceeds then rides the channels connection into the
|
||||||
//! fabric, whose own `AccessControl::check(peer_identity)` gates the
|
//! fabric, whose own `AccessControl::check(peer_identity)` gates the
|
||||||
//! mapped `channels/tunnel/direct` open (alkcall CF-005/CF-006) —
|
//! mapped `channels/tunnel/direct` open (alkcall CF-005/CF-006);
|
||||||
//! the composed-gate witness is the harness's identity rows. The
|
//! the composed-gate witness is the harness's identity rows. The
|
||||||
//! `-R` leg composes the same gates on both of its directions: the
|
//! `-R` leg composes the same gates on both of its directions: the
|
||||||
//! listen dial rides the fabric leg's identity (`tunnel:open` scope —
|
//! listen dial rides the fabric leg's identity (`tunnel:open` scope,
|
||||||
//! the assembly's dial-identity composition, unchanged), and the
|
//! the assembly's dial-identity composition, unchanged), and the
|
||||||
//! arrivals are gated by the door-side registry's ACL
|
//! arrivals are gated by the door-side registry's ACL
|
||||||
//! (`tunnel:forwarded` scope, per arrival) exactly when the assembly's
|
//! (`tunnel:forwarded` scope, per arrival) exactly when the assembly's
|
||||||
//! fabric leg carries it.
|
//! fabric leg carries it.
|
||||||
//!
|
//!
|
||||||
//! **`set_identity` mechanics (recorded — `w3-tunnel-direct`'s decided
|
//! **`set_identity` mechanics (recorded: `w3-tunnel-direct`'s decided
|
||||||
//! point, carried unchanged):** the alkhub `hub_leg.rs` one-time-set
|
//! point, carried unchanged):** the alkhub `hub_leg.rs` one-time-set
|
||||||
//! precedent (`channel0_conn.set_identity(identity)`) is a
|
//! precedent (`channel0_conn.set_identity(identity)`) is a
|
||||||
//! **serving-side** set — the identity the fabric leg dials with, and
|
//! **serving-side** set: the identity the fabric leg dials with, and
|
||||||
//! against which the fabric's ACL gates alkssh's mapped opens, is the
|
//! against which the fabric's ACL gates alkssh's mapped opens, is the
|
||||||
//! assembly-supplied `ChannelClient`'s own dial identity (CF-005: set
|
//! assembly-supplied `ChannelClient`'s own dial identity (CF-005: set
|
||||||
//! once at dial, propagated to channel 0 at `from_connection`); the
|
//! once at dial, propagated to channel 0 at `from_connection`); the
|
||||||
@@ -216,7 +228,10 @@ use alkcall::core::auth::AuthContext;
|
|||||||
use alkcall::core::types::BiStream;
|
use alkcall::core::types::BiStream;
|
||||||
use alktunnels::consumer::{TunnelListener, TunnelSession};
|
use alktunnels::consumer::{TunnelListener, TunnelSession};
|
||||||
use alktunnels::params::{Substrate, SubstrateAddr, TunnelForwardedParams, TunnelParams};
|
use alktunnels::params::{Substrate, SubstrateAddr, TunnelForwardedParams, TunnelParams};
|
||||||
use alktunnels::producer::{ForwardedHandler, register_tunnel_forwarded_openable};
|
use alktunnels::producer::{
|
||||||
|
ForwardedGate, ForwardedHandler, TunnelEstablishError,
|
||||||
|
register_tunnel_forwarded_openable_with_establisher,
|
||||||
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use russh::Channel;
|
use russh::Channel;
|
||||||
use russh::server::{ChannelOpenHandle, Handle};
|
use russh::server::{ChannelOpenHandle, Handle};
|
||||||
@@ -231,10 +246,10 @@ use crate::{config, dispatch, observe};
|
|||||||
pub(crate) type WireChannel = Channel<russh::server::Msg>;
|
pub(crate) type WireChannel = Channel<russh::server::Msg>;
|
||||||
|
|
||||||
/// The spawned route-1 bridge (`w3-tunnel-direct`, the POC-3 §C
|
/// The spawned route-1 bridge (`w3-tunnel-direct`, the POC-3 §C
|
||||||
/// recipe): the fabric open runs here — accept only after it succeeds,
|
/// recipe): the fabric open runs here; accept only after it succeeds,
|
||||||
/// `take_halves()` pumped against the taken channel on completion, and
|
/// `take_halves()` pumped against the taken channel on completion, and
|
||||||
/// on failure the explicit `ConnectFailed` reject **with the cap count
|
/// on failure the explicit `ConnectFailed` reject **with the cap count
|
||||||
/// released** (the recorded release mechanism — the admitted open this
|
/// released** (the recorded release mechanism: the admitted open this
|
||||||
/// task holds was never established, so no `channel_close` can ever
|
/// task holds was never established, so no `channel_close` can ever
|
||||||
/// fire; the count recovers here, in the task that knows the failure,
|
/// fire; the count recovers here, in the task that knows the failure,
|
||||||
/// exactly once: a success never releases). The channel closes on
|
/// exactly once: a success never releases). The channel closes on
|
||||||
@@ -268,7 +283,7 @@ pub(crate) async fn serve_direct_row(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The open hook's spawn step (ADR-006 decision 4): the handler clones
|
/// The open hook's spawn step (ADR-006 decision 4): the handler clones
|
||||||
/// the cap `Arc` (the failure-path release's handle — the count was
|
/// the cap `Arc` (the failure-path release's handle; the count was
|
||||||
/// admitted at the hook and is held across the fabric open) and hands
|
/// admitted at the hook and is held across the fabric open) and hands
|
||||||
/// the rest over; the hook returns fast with the fabric open in
|
/// the rest over; the hook returns fast with the fabric open in
|
||||||
/// flight.
|
/// flight.
|
||||||
@@ -292,29 +307,29 @@ pub(crate) fn spawn_direct_row(
|
|||||||
|
|
||||||
/// The wall-clock bound on the family-ward listen dial (`TunnelListener::
|
/// The wall-clock bound on the family-ward listen dial (`TunnelListener::
|
||||||
/// open`): the fabric's establisher is already bound by alkcall's own
|
/// open`): the fabric's establisher is already bound by alkcall's own
|
||||||
/// establisher deadline (10 s, alkcall ADR-049 §2), so this matches it —
|
/// establisher deadline (10 s, alkcall ADR-049 §2), so this matches it,
|
||||||
/// a fabric that never answers a listen dial cannot hold the packet
|
/// a fabric that never answers a listen dial cannot hold the packet
|
||||||
/// loop longer than a bounded dial.
|
/// loop longer than a bounded dial.
|
||||||
pub(crate) const LISTEN_DIAL_TIMEOUT: Duration = Duration::from_secs(10);
|
pub(crate) const LISTEN_DIAL_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
/// One route in the listen-arrival bookkeeping — registered at listen
|
/// One route in the listen-arrival bookkeeping, registered at listen
|
||||||
/// acceptance, consulted per arrival. The echo fields are the vanilla
|
/// acceptance, consulted per arrival. The echo fields are the vanilla
|
||||||
/// `-R` convention decoded (sshd parity, recorded on [`ListenRouter`]):
|
/// `-R` convention decoded (sshd parity, recorded on [`ListenRouter`]):
|
||||||
/// the connected address rides the wire `tcpip_forward` string
|
/// the connected address rides the wire `tcpip_forward` string
|
||||||
/// **verbatim** (the OpenSSH client sent that exact string and matches
|
/// **verbatim** (the OpenSSH client sent that exact string and matches
|
||||||
/// the open's address field against it exactly), the port is the
|
/// the open's address field against it exactly), the port is the
|
||||||
/// **actually bound** one (the fabric's `bound` reply — the sshd
|
/// **actually bound** one (the fabric's `bound` reply, the sshd
|
||||||
/// `get_local_port`-of-accepted-socket parity, which is what makes
|
/// `get_local_port`-of-accepted-socket parity, which is what makes
|
||||||
/// `port = 0` mints round-trip), and the unix path rides the
|
/// `port = 0` mints round-trip), and the unix path rides the
|
||||||
/// `streamlocal_forward` request verbatim.
|
/// `streamlocal_forward` request verbatim.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct ListenRoute {
|
pub(crate) struct ListenRoute {
|
||||||
/// The SSH args the arrivals' opens echo — keyed by the route's
|
/// The SSH args the arrivals' opens echo, keyed by the route's
|
||||||
/// substrate (`Tcp`/`Udp` → the vanilla `-R` trio's first two
|
/// substrate (`Tcp`/`Udp` → the vanilla `-R` trio's first two
|
||||||
/// fields; `Unix` → the `forwarded-streamlocal` path).
|
/// fields; `Unix` → the `forwarded-streamlocal` path).
|
||||||
pub(crate) echo: RouteEcho,
|
pub(crate) echo: RouteEcho,
|
||||||
pub(crate) substrate: Substrate,
|
pub(crate) substrate: Substrate,
|
||||||
/// The owning session's packet-loop handle — the arrival's SSH
|
/// The owning session's packet-loop handle: the arrival's SSH
|
||||||
/// channel opens through it; a session that died fails the open
|
/// channel opens through it; a session that died fails the open
|
||||||
/// (the arrival's teardown-at-birth path).
|
/// (the arrival's teardown-at-birth path).
|
||||||
pub(crate) handle: Arc<Handle>,
|
pub(crate) handle: Arc<Handle>,
|
||||||
@@ -340,10 +355,10 @@ pub(crate) enum RouteEcho {
|
|||||||
/// `Drop`). A mint on an already-held token is refused fail-closed
|
/// `Drop`). A mint on an already-held token is refused fail-closed
|
||||||
/// ([`ListenRouter::register`], `w4-router-collision`). Arrivals
|
/// ([`ListenRouter::register`], `w4-router-collision`). Arrivals
|
||||||
/// that find no route are the correlation-miss
|
/// that find no route are the correlation-miss
|
||||||
/// class — teardown-at-birth, never a half-open SSH channel.
|
/// class (teardown-at-birth, never a half-open SSH channel).
|
||||||
///
|
///
|
||||||
/// The map is behind a `parking_lot::Mutex` because arrivals arrive on
|
/// The map is behind a `parking_lot::Mutex` because arrivals arrive on
|
||||||
/// alkcall serving tasks while the packet loop mints/removes — the
|
/// alkcall serving tasks while the packet loop mints/removes; the
|
||||||
/// held region is a HashMap op, never an await.
|
/// held region is a HashMap op, never an await.
|
||||||
pub(crate) struct ListenRouter {
|
pub(crate) struct ListenRouter {
|
||||||
routes: Mutex<HashMap<u32, ListenRoute>>,
|
routes: Mutex<HashMap<u32, ListenRoute>>,
|
||||||
@@ -352,21 +367,21 @@ pub(crate) struct ListenRouter {
|
|||||||
impl ListenRouter {
|
impl ListenRouter {
|
||||||
/// Minted-entry registration (the listen-acceptance step),
|
/// Minted-entry registration (the listen-acceptance step),
|
||||||
/// fail-closed on token collision (the wave-3 gate's booked item
|
/// fail-closed on token collision (the wave-3 gate's booked item
|
||||||
/// 4, `w4-router-collision` — the same posture the same gate
|
/// 4, `w4-router-collision`, the same posture the same gate
|
||||||
/// landed inline for a duplicate `tunnel/forwarded`
|
/// landed inline for a duplicate `tunnel/forwarded`
|
||||||
/// registration): safety rests on the fabric's ADR-008 mint
|
/// registration): safety rests on the fabric's ADR-008 mint
|
||||||
/// discipline upstream, and this defends in depth against a
|
/// discipline upstream, and this defends in depth against a
|
||||||
/// collided mint — a `register` on an already-held token returns
|
/// collided mint, a `register` on an already-held token returns
|
||||||
/// `Err` and **never replaces** the held route, so a colliding
|
/// `Err` and **never replaces** the held route, so a colliding
|
||||||
/// mint can never steal a live adapter's arrival routes. The
|
/// mint can never steal a live adapter's arrival routes. The
|
||||||
/// caller declines the listen (the fabric listener is then torn
|
/// caller declines the listen (the fabric listener is then torn
|
||||||
/// down — a refused registration never left a half state).
|
/// down; a refused registration never left a half state).
|
||||||
///
|
///
|
||||||
/// Error surface (decided, `w4-router-collision`): the refusal
|
/// Error surface (decided, `w4-router-collision`): the refusal
|
||||||
/// rides [`String`]-carried detail surfaced as the request-time
|
/// rides [`String`]-carried detail surfaced as the request-time
|
||||||
/// **decline disposition** (`RequestDenied`, reason
|
/// **decline disposition** (`RequestDenied`, reason
|
||||||
/// `token-collision`), not a distinct [`AdapterError`](crate::AdapterError)
|
/// `token-collision`), not a distinct [`AdapterError`](crate::AdapterError)
|
||||||
/// variant — a global-request hook's `Err` would kill the whole
|
/// variant, since a global-request hook's `Err` would kill the whole
|
||||||
/// russh session (russh 0.65 answers the decline from the hook's
|
/// russh session (russh 0.65 answers the decline from the hook's
|
||||||
/// `Ok(false)`, and `Err` propagates out of the packet loop), and
|
/// `Ok(false)`, and `Err` propagates out of the packet loop), and
|
||||||
/// the taxonomy's rule holds that wire decline reasons are never
|
/// the taxonomy's rule holds that wire decline reasons are never
|
||||||
@@ -378,7 +393,7 @@ impl ListenRouter {
|
|||||||
let mut routes = self.routes.lock();
|
let mut routes = self.routes.lock();
|
||||||
if routes.contains_key(&token) {
|
if routes.contains_key(&token) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"listen token {token} is already routed — a collided \
|
"listen token {token} is already routed; a collided \
|
||||||
mint may not replace a live adapter's route"
|
mint may not replace a live adapter's route"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -386,7 +401,7 @@ impl ListenRouter {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The per-arrival lookup — cloned out, never taken: one route
|
/// The per-arrival lookup, cloned out, never taken: one route
|
||||||
/// serves unboundedly many arrivals; only the cancel forms and
|
/// serves unboundedly many arrivals; only the cancel forms and
|
||||||
/// session teardown remove.
|
/// session teardown remove.
|
||||||
pub(crate) fn route_for(&self, token: u32) -> Option<ListenRoute> {
|
pub(crate) fn route_for(&self, token: u32) -> Option<ListenRoute> {
|
||||||
@@ -394,7 +409,7 @@ impl ListenRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The cancel-form removal (1:1 into the family listener's
|
/// The cancel-form removal (1:1 into the family listener's
|
||||||
/// teardown): removes first, so a racing arrival already misses —
|
/// teardown): removes first, so a racing arrival already misses,
|
||||||
/// teardown-at-birth, never a post-cancel half-serve.
|
/// teardown-at-birth, never a post-cancel half-serve.
|
||||||
pub(crate) fn cancel(&self, token: &u32) -> bool {
|
pub(crate) fn cancel(&self, token: &u32) -> bool {
|
||||||
self.routes.lock().remove(token).is_some()
|
self.routes.lock().remove(token).is_some()
|
||||||
@@ -402,7 +417,7 @@ impl ListenRouter {
|
|||||||
|
|
||||||
/// The owning session's teardown (the handler's `Drop`): remove
|
/// The owning session's teardown (the handler's `Drop`): remove
|
||||||
/// the session's tokens; any listener objects left drop with the
|
/// the session's tokens; any listener objects left drop with the
|
||||||
/// handler (the fabric-side reap — see `PendingListen`).
|
/// handler (the fabric-side reap, see `PendingListen`).
|
||||||
pub(crate) fn remove_tokens(&self, tokens: &[u32]) {
|
pub(crate) fn remove_tokens(&self, tokens: &[u32]) {
|
||||||
let mut routes = self.routes.lock();
|
let mut routes = self.routes.lock();
|
||||||
for token in tokens {
|
for token in tokens {
|
||||||
@@ -423,24 +438,42 @@ impl Default for ListenRouter {
|
|||||||
/// [`crate::SshAdapter::new`] when the fabric leg + seam are both
|
/// [`crate::SshAdapter::new`] when the fabric leg + seam are both
|
||||||
/// present): builds the router, registers alktunnels' forwarded
|
/// present): builds the router, registers alktunnels' forwarded
|
||||||
/// openable into the seam's shared registry over a `ChannelCore`
|
/// openable into the seam's shared registry over a `ChannelCore`
|
||||||
/// wrapping a clone of the held `ChannelClient`'s manager — the
|
/// wrapping a clone of the held `ChannelClient`'s manager, the
|
||||||
/// alkcall shape whose wrapper performs the door-side arrival's
|
/// alkcall shape whose wrapper performs the door-side arrival's
|
||||||
/// channel allocation, ACL (`tunnel:forwarded`, per call) and budget
|
/// channel allocation, ACL (`tunnel:forwarded`, per call) and budget
|
||||||
/// (alkcall's secure default per-identity policy, `256`, plus the
|
/// (alkcall's secure default per-identity policy, `256`, plus the
|
||||||
/// `DEFAULT_MAX_CHANNELS` per-connection bound — alkssh composes no
|
/// `DEFAULT_MAX_CHANNELS` per-connection bound; alkssh composes no
|
||||||
/// deployment policy; an assembly wanting a different per-identity
|
/// deployment policy; an assembly wanting a different per-identity
|
||||||
/// cap shapes it through its own `ServingConfig`/`registry`
|
/// cap shapes it through its own `ServingConfig`/`registry`
|
||||||
/// composition upstream) — and spawns alktunnels' forward-op handler
|
/// composition upstream), and spawns alktunnels' forward-op handler
|
||||||
/// over the crate's arrival pump. The registration is
|
/// over the crate's arrival pump.
|
||||||
|
///
|
||||||
|
/// The registration shape is the **establisher variant**
|
||||||
|
/// (`register_tunnel_forwarded_openable_with_establisher`, alktunnels
|
||||||
|
/// 0.5.0, review 003 W2's adoption, ADR-008 Amendment 2's recommended
|
||||||
|
/// form): the router's correlation lookup rides a [`ForwardedGate`]
|
||||||
|
/// capturing the router, so a stale/missing `listen` token fails the
|
||||||
|
/// open at alkcall's call plane as a typed `channel:open_failed`
|
||||||
|
/// (`details.reason == "unknown_resource"`) **before any channel is
|
||||||
|
/// born**; the phantom-channel class (call-plane success, then
|
||||||
|
/// handler-side decline) is closed at the open; door-side arrival
|
||||||
|
/// correlations that pass the gate but lose the narrow gate-pass→
|
||||||
|
/// handler-run race, plus every cap-admission decline, still take the
|
||||||
|
/// handler's teardown-at-birth path (the residual-race posture the
|
||||||
|
/// base registration also carries). A malformed arrival is likewise
|
||||||
|
/// typed (`handler_error`) instead of the base shape's
|
||||||
|
/// success-then-silent-birth-teardown.
|
||||||
|
///
|
||||||
|
/// The registration is
|
||||||
/// one-per-op-id-per-registry: alkcall's `register` silently
|
/// one-per-op-id-per-registry: alkcall's `register` silently
|
||||||
/// replaces an existing entry, so the door **pre-checks the op** —
|
/// replaces an existing entry, so the door **pre-checks the op**:
|
||||||
/// a `tunnel/forwarded` registration that already exists (e.g. a
|
/// a `tunnel/forwarded` registration that already exists (e.g. a
|
||||||
/// second adapter over the same shared registry) is a failed
|
/// second adapter over the same shared registry) is a failed
|
||||||
/// construction (fail-closed — never a route to a handler the door
|
/// construction (fail-closed; never a route to a handler the door
|
||||||
/// does not own).
|
/// does not own).
|
||||||
///
|
///
|
||||||
/// On success the router rides back into the serve-config (the
|
/// On success the router rides back into the serve-config (the
|
||||||
/// session handlers read it) — `Ok` with the router installed, `Err`
|
/// session handlers read it): `Ok` with the router installed, `Err`
|
||||||
/// is the caller's constructor failure.
|
/// is the caller's constructor failure.
|
||||||
pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Result<(), String> {
|
pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Result<(), String> {
|
||||||
let Some(seam) = serve.tunnel_listen_seam.as_mut() else {
|
let Some(seam) = serve.tunnel_listen_seam.as_mut() else {
|
||||||
@@ -450,6 +483,21 @@ pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Resu
|
|||||||
return Err("tunnel listen seam configured without a fabric leg".to_string());
|
return Err("tunnel listen seam configured without a fabric leg".to_string());
|
||||||
};
|
};
|
||||||
let router = Arc::new(ListenRouter::default());
|
let router = Arc::new(ListenRouter::default());
|
||||||
|
let gate: ForwardedGate = {
|
||||||
|
let router = Arc::clone(&router);
|
||||||
|
Arc::new(move |params: &TunnelForwardedParams| {
|
||||||
|
if router.route_for(params.listen).is_some() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(TunnelEstablishError::UnknownResource(format!(
|
||||||
|
"no accepted listen routes the arrival's token {}, \
|
||||||
|
the forwarding session is gone or was never minted \
|
||||||
|
(alkssh: stale/missing `-R` token)",
|
||||||
|
params.listen
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
let handler: ForwardedHandler = {
|
let handler: ForwardedHandler = {
|
||||||
let router = Arc::clone(&router);
|
let router = Arc::clone(&router);
|
||||||
Arc::new(move |params: TunnelForwardedParams, bidi: BiStream| {
|
Arc::new(move |params: TunnelForwardedParams, bidi: BiStream| {
|
||||||
@@ -467,15 +515,16 @@ pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Resu
|
|||||||
{
|
{
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"the tunnel/forwarded op ({}) is already registered in the \
|
"the tunnel/forwarded op ({}) is already registered in the \
|
||||||
seam's registry — a conflicting registration would silently \
|
seam's registry; a conflicting registration would silently \
|
||||||
replace the door's arrival route",
|
replace the door's arrival route",
|
||||||
alktunnels::params::OP_TUNNEL_FORWARDED
|
alktunnels::params::OP_TUNNEL_FORWARDED
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
register_tunnel_forwarded_openable(
|
register_tunnel_forwarded_openable_with_establisher(
|
||||||
&core,
|
&core,
|
||||||
&seam.registry,
|
&seam.registry,
|
||||||
AuthContext::anonymous(b"alk/tunnel"),
|
AuthContext::anonymous(b"alk/tunnel"),
|
||||||
|
gate,
|
||||||
handler,
|
handler,
|
||||||
)
|
)
|
||||||
.map_err(|failure| format!("tunnel/forwarded openable registration: {failure}"))?;
|
.map_err(|failure| format!("tunnel/forwarded openable registration: {failure}"))?;
|
||||||
@@ -483,29 +532,34 @@ pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Resu
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The registered arrival pump — the `forwarded-tcpip` data leg
|
/// The registered arrival pump, the `forwarded-tcpip` data leg
|
||||||
/// (mapping.md's row, "hand the channel duplex to the handler"): the
|
/// (mapping.md's row, "hand the channel duplex to the handler"): the
|
||||||
/// alkcall openable registration hands every fabric arrival here with
|
/// alkcall openable registration hands every fabric arrival here with
|
||||||
/// the parsed [`TunnelForwardedParams`] and the arrival's `BiStream`;
|
/// the parsed [`TunnelForwardedParams`] and the arrival's `BiStream`;
|
||||||
/// the flow is the recorded shape:
|
/// the flow is the recorded shape:
|
||||||
///
|
///
|
||||||
/// 1. **Correlation** — the route for the arrival's minted token;
|
/// 1. **Correlation**: the route for the arrival's minted token;
|
||||||
/// a miss is teardown-at-birth (drop the fabric half; the alkcall
|
/// a miss is teardown-at-birth (drop the fabric half; the alkcall
|
||||||
/// wrapper's awaited join ends, the channel reaps).
|
/// wrapper's awaited join ends, the channel reaps). The gate
|
||||||
|
/// (the establisher's call-plane correlation) misses a stale
|
||||||
|
/// token before any channel is born; this handler-side re-lookup
|
||||||
|
/// is the narrow gate-pass→handler-run **residual race**, the
|
||||||
|
/// same miss class, past the open, and the never-established cap
|
||||||
|
/// declines below take the same path.
|
||||||
/// 2. **The in-crate cap admission** (bounded resources at the accept
|
/// 2. **The in-crate cap admission** (bounded resources at the accept
|
||||||
/// surface — the wave-2 gate's booked acceptor vocabulary): no
|
/// surface, the wave-2 gate's booked acceptor vocabulary): no
|
||||||
/// room → the fabric half drops, the arrival never opens an SSH
|
/// room → the fabric half drops, the arrival never opens an SSH
|
||||||
/// channel. The count releases here on every never-established
|
/// channel. The count releases here on every never-established
|
||||||
/// path; an established one releases at its close (the packet
|
/// path; an established one releases at its close (the packet
|
||||||
/// loop's release site), the direct rows' disjoint-release shape.
|
/// loop's release site), the direct rows' disjoint-release shape.
|
||||||
/// 3. **The SSH open toward the client** — the echoed listen args
|
/// 3. **The SSH open toward the client**, the echoed listen args
|
||||||
/// (`forwarded-tcpip` for the tcp/udp substrates with the originator
|
/// (`forwarded-tcpip` for the tcp/udp substrates with the originator
|
||||||
/// fields mapped from `TunnelForwardedParams.peer`; `forwarded-
|
/// fields mapped from `TunnelForwardedParams.peer`; `forwarded-
|
||||||
/// streamlocal@openssh.com` with the echoed path for `Unix`) — per
|
/// streamlocal@openssh.com` with the echoed path for `Unix`); per
|
||||||
/// the spawn-and-return constraint this runs on this spawned task;
|
/// the spawn-and-return constraint this runs on this spawned task;
|
||||||
/// a rejected/dead-open releases the count and drops the fabric
|
/// a rejected/dead-open releases the count and drops the fabric
|
||||||
/// half (never a half-open SSH channel).
|
/// half (never a half-open SSH channel).
|
||||||
/// 4. **The pump** — the fabric `BiStream` ↔ the SSH channel duplex
|
/// 4. **The pump**: the fabric `BiStream` ↔ the SSH channel duplex
|
||||||
/// (the route-1 bridge's primitive, reversed).
|
/// (the route-1 bridge's primitive, reversed).
|
||||||
async fn pump_forwarded_arrival(
|
async fn pump_forwarded_arrival(
|
||||||
router: Arc<ListenRouter>,
|
router: Arc<ListenRouter>,
|
||||||
@@ -555,7 +609,7 @@ async fn pump_forwarded_arrival(
|
|||||||
}
|
}
|
||||||
(RouteEcho::Tcp { address, port }, SubstrateAddr::Path { path }) => {
|
(RouteEcho::Tcp { address, port }, SubstrateAddr::Path { path }) => {
|
||||||
// The degenerate-but-typed mapping (a path-shaped peer on
|
// The degenerate-but-typed mapping (a path-shaped peer on
|
||||||
// a tcp listen — unreachable from a well-behaved producer):
|
// a tcp listen, unreachable from a well-behaved producer):
|
||||||
// the path rides the originator string, port 0.
|
// the path rides the originator string, port 0.
|
||||||
route
|
route
|
||||||
.handle
|
.handle
|
||||||
@@ -584,12 +638,12 @@ async fn pump_forwarded_arrival(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A minted listen — the door-side outcome of a `tcpip_forward` /
|
/// A minted listen, the door-side outcome of a `tcpip_forward` /
|
||||||
/// `streamlocal_forward` request's family-ward dial (the published
|
/// `streamlocal_forward` request's family-ward dial (the published
|
||||||
/// `TunnelListener::open`, bounded by [`LISTEN_DIAL_TIMEOUT`], with
|
/// `TunnelListener::open`, bounded by [`LISTEN_DIAL_TIMEOUT`], with
|
||||||
/// the reply's `bound` + `listen` establisher fields required: a
|
/// the reply's `bound` + `listen` establisher fields required: a
|
||||||
/// producer that minted no correlation token or no bound report
|
/// producer that minted no correlation token or no bound report
|
||||||
/// cannot serve — [`Result`]'s `Err` declines the SSH request before
|
/// cannot serve; [`Result`]'s `Err` declines the SSH request before
|
||||||
/// anything registers).
|
/// anything registers).
|
||||||
pub(crate) struct MintedListen {
|
pub(crate) struct MintedListen {
|
||||||
pub(crate) listener: TunnelListener,
|
pub(crate) listener: TunnelListener,
|
||||||
@@ -599,9 +653,9 @@ pub(crate) struct MintedListen {
|
|||||||
|
|
||||||
/// The listen dial (the mapping.md §Global-request rows' family-ward
|
/// The listen dial (the mapping.md §Global-request rows' family-ward
|
||||||
/// step): `TunnelParams { resource: <endpoint string>, substrate }`
|
/// step): `TunnelParams { resource: <endpoint string>, substrate }`
|
||||||
/// — the wire-named bind endpoint as the resource, the recorded
|
/// : the wire-named bind endpoint as the resource, the recorded
|
||||||
/// door-side naming convention whose fabric-side interpretation is
|
/// door-side naming convention whose fabric-side interpretation is
|
||||||
/// the assembly's establisher — then the reply projection. Called
|
/// the assembly's establisher; then the reply projection. Called
|
||||||
/// inline from the handler hook (see the recorded inline-decision in
|
/// inline from the handler hook (see the recorded inline-decision in
|
||||||
/// the module doc).
|
/// the module doc).
|
||||||
pub(crate) async fn open_listen(
|
pub(crate) async fn open_listen(
|
||||||
@@ -636,7 +690,7 @@ pub(crate) async fn open_listen(
|
|||||||
|
|
||||||
/// The bound port the listen request's out-param carries
|
/// The bound port the listen request's out-param carries
|
||||||
/// (the `&mut u32` sshd parity): the fabric's bound report's port.
|
/// (the `&mut u32` sshd parity): the fabric's bound report's port.
|
||||||
/// Unix listens have no port semantics — the tcp variants only.
|
/// Unix listens have no port semantics; the tcp variants only.
|
||||||
pub(crate) fn bound_port(bound: &SubstrateAddr) -> Option<u32> {
|
pub(crate) fn bound_port(bound: &SubstrateAddr) -> Option<u32> {
|
||||||
match bound {
|
match bound {
|
||||||
SubstrateAddr::HostPort { port, .. } => Some(u32::from(*port)),
|
SubstrateAddr::HostPort { port, .. } => Some(u32::from(*port)),
|
||||||
@@ -644,12 +698,12 @@ pub(crate) fn bound_port(bound: &SubstrateAddr) -> Option<u32> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The family-ward resource string for a listen request — the wire
|
/// The family-ward resource string for a listen request: the wire
|
||||||
/// bind endpoint verbatim (`"localhost"` / `"0.0.0.0"` / explicit
|
/// bind endpoint verbatim (`"localhost"` / `"0.0.0.0"` / explicit
|
||||||
/// host: the OpenSSH client has already normalized the no-address case
|
/// host: the OpenSSH client has already normalized the no-address case
|
||||||
/// to `"localhost"` and `""` before this surface exists — sshd
|
/// to `"localhost"` and `""` before this surface exists; sshd
|
||||||
/// `channel_rfwd_bind_host` parity), with the requested port for the
|
/// `channel_rfwd_bind_host` parity), with the requested port for the
|
||||||
/// tcp substrates (`host:port`; port `0` stays `0` — the OS-picking is
|
/// tcp substrates (`host:port`; port `0` stays `0`, since the OS-picking is
|
||||||
/// the fabric's bind step, whose bound report feeds the out-param).
|
/// the fabric's bind step, whose bound report feeds the out-param).
|
||||||
pub(crate) fn listen_resource(endpoint: &str, substrate: Substrate, port: Option<u32>) -> String {
|
pub(crate) fn listen_resource(endpoint: &str, substrate: Substrate, port: Option<u32>) -> String {
|
||||||
match substrate {
|
match substrate {
|
||||||
@@ -661,9 +715,9 @@ pub(crate) fn listen_resource(endpoint: &str, substrate: Substrate, port: Option
|
|||||||
/// The per-session pending listen (the packet-loop-only store, like
|
/// The per-session pending listen (the packet-loop-only store, like
|
||||||
/// the `channels`/`ptys` stores): the minted token is the map's key;
|
/// the `channels`/`ptys` stores): the minted token is the map's key;
|
||||||
/// the entry carries what the cancel forms match on (the wire endpoint
|
/// the entry carries what the cancel forms match on (the wire endpoint
|
||||||
/// verbatim + the bound port — the sshd `c->path == host &&
|
/// verbatim + the bound port, the sshd `c->path == host &&
|
||||||
/// c->listening_port == port` parity) over the substrate (a unix
|
/// c->listening_port == port` parity) over the substrate (a unix
|
||||||
/// entry's port is 0-carrying and never matched — the unix cancel
|
/// entry's port is 0-carrying and never matched; the unix cancel
|
||||||
/// forms match on the path alone) and the session-owned
|
/// forms match on the path alone) and the session-owned
|
||||||
/// [`TunnelListener`] whose close/Drop performs the family teardown.
|
/// [`TunnelListener`] whose close/Drop performs the family teardown.
|
||||||
pub(crate) struct PendingListen {
|
pub(crate) struct PendingListen {
|
||||||
@@ -673,16 +727,65 @@ pub(crate) struct PendingListen {
|
|||||||
pub(crate) listener: TunnelListener,
|
pub(crate) listener: TunnelListener,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The listen count's dedicated bound — the same mechanism as
|
/// The listen count's dedicated bound, the same mechanism as
|
||||||
/// [`ChannelCap`], instantiated per session next to it, against the
|
/// [`ChannelCap`], instantiated per session next to it, against the
|
||||||
/// same `Limits::max_concurrent_channels` ceiling: listeners and
|
/// same `Limits::max_concurrent_channels` ceiling: listeners and
|
||||||
/// channels stay independently bounded while their mix stays
|
/// channels stay independently bounded while their mix stays
|
||||||
/// unconstrained (AGENTS.md convention 9 — no unbounded listener
|
/// unconstrained (AGENTS.md convention 9: no unbounded listener
|
||||||
/// map). Admits are packet-loop-only (the listen hooks are the only
|
/// map). Admits are packet-loop-only (the listen hooks are the only
|
||||||
/// mint site); releases fire at the cancel forms, the listen-dial
|
/// mint site); releases fire at the cancel forms, the listen-dial
|
||||||
/// failures (admitted-but-never-minted — the route 1 bridge's
|
/// failures (admitted-but-never-minted, the route 1 bridge's
|
||||||
/// release-site shape), and nowhere else (a minted listener's release
|
/// release-site shape), and nowhere else (a minted listener's release
|
||||||
/// is... never — listeners hold until cancel or session teardown, so
|
/// is... never: listeners hold until cancel or session teardown, so
|
||||||
/// the session's count dies with the handler, per the channels
|
/// the session's count dies with the handler, per the channels
|
||||||
/// cap's same posture).
|
/// cap's same posture).
|
||||||
pub(crate) type ListenerCap = ChannelCap;
|
pub(crate) type ListenerCap = ChannelCap;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use alktunnels::params::{Substrate, SubstrateAddr, TunnelForwardedParams};
|
||||||
|
|
||||||
|
use super::{ListenRouter, pump_forwarded_arrival};
|
||||||
|
|
||||||
|
/// The arrival pump's handler-side miss (the narrow gate-pass→
|
||||||
|
/// handler-run residual race the establisher shape retains) still
|
||||||
|
/// teardown-at-births: a token with no route at handler time drops
|
||||||
|
/// the fabric half; the adoptee reads EOF, never a half-served
|
||||||
|
/// arrival.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn the_pump_residual_miss_teardown_at_births_the_fabric_half() {
|
||||||
|
let router = ListenRouter::default();
|
||||||
|
let (door, fabric_half) = tokio::io::duplex(64 * 1024);
|
||||||
|
let (reader, writer) = tokio::io::split(door);
|
||||||
|
|
||||||
|
let bidi = alkcall::core::types::BiStream::from_joined(reader, writer);
|
||||||
|
|
||||||
|
let pump = pump_forwarded_arrival(
|
||||||
|
std::sync::Arc::new(router),
|
||||||
|
TunnelForwardedParams {
|
||||||
|
listen: 424_242,
|
||||||
|
substrate: Substrate::Tcp,
|
||||||
|
peer: SubstrateAddr::HostPort {
|
||||||
|
host: "127.0.0.1".to_owned(),
|
||||||
|
port: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bidi,
|
||||||
|
);
|
||||||
|
tokio::time::timeout(std::time::Duration::from_secs(10), pump)
|
||||||
|
.await
|
||||||
|
.expect("the miss returns in time (the pump drops the half and ends)");
|
||||||
|
|
||||||
|
let mut adoptee = fabric_half;
|
||||||
|
let read = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
tokio::io::AsyncReadExt::read_u8(&mut adoptee),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("the adoptee's EOF is observable in time");
|
||||||
|
assert!(
|
||||||
|
read.is_err(),
|
||||||
|
"the adopted fabric half EOFs (teardown-at-birth, not a half-serve)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: architecture/oq-01-relay-leg-consumer
|
id: architecture/oq-01-relay-leg-consumer
|
||||||
name: "OQ-01 tracker — alk/ssh relay/browser-leg consumer"
|
name: "OQ-01 tracker: alk/ssh relay/browser-leg consumer"
|
||||||
status: pending
|
status: pending
|
||||||
depends_on: []
|
depends_on: []
|
||||||
scope: single
|
scope: single
|
||||||
@@ -12,11 +12,11 @@ tags: [deferred-oq, external-trigger]
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
External-trigger tracker for [OQ-01](docs/architecture/open-questions.md) — "does an `alk/ssh` relay/browser-leg consumer emerge?" (deferred(scope)). Not actionable work; tracks whether the unblocking condition has arrived.
|
External-trigger tracker for [OQ-01](docs/architecture/open-questions.md): "does an `alk/ssh` relay/browser-leg consumer emerge?" (deferred(scope)). Not actionable work; it tracks whether the unblocking condition has arrived.
|
||||||
|
|
||||||
## Work
|
## Work
|
||||||
|
|
||||||
Transition check only: when a concrete consumer use case exists — a browser-WASM route landing in an assembly (alkcall ADR-009 sketch) or a hub deployment needing SSH-in-relay — mark this task `completed` and move OQ-01 from `deferred(scope)` to `open` in `open-questions.md` (directly to `resolved` if no decision remains, which would be an ADR-001 amendment since the pin is one-way).
|
Transition check only: when a concrete consumer use case exists, either a browser-WASM route landing in an assembly (alkcall ADR-009 sketch) or a hub deployment needing SSH-in-relay, mark this task `completed` and move OQ-01 from `deferred(scope)` to `open` in `open-questions.md` (directly to `resolved` if no decision remains, which would be an ADR-001 amendment since the pin is one-way).
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ Transition check only: when a concrete consumer use case exists — a browser-WA
|
|||||||
|
|
||||||
## Out of scope
|
## Out of scope
|
||||||
|
|
||||||
Any actual relay/browser-leg design — that work would be new ADR/spec territory once the use case exists.
|
Any actual relay/browser-leg design; that work would be new ADR/spec territory once the use case exists.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: architecture/oq-04-openssh-cert-auth
|
id: architecture/oq-04-openssh-cert-auth
|
||||||
name: "OQ-04 tracker — OpenSSH certificate auth use case"
|
name: "OQ-04 tracker: OpenSSH certificate auth use case"
|
||||||
status: pending
|
status: pending
|
||||||
depends_on: []
|
depends_on: []
|
||||||
scope: single
|
scope: single
|
||||||
@@ -12,7 +12,7 @@ tags: [deferred-oq, external-trigger]
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
External-trigger tracker for [OQ-04](docs/architecture/open-questions.md) — whether an OpenSSH-CA-trust use case ever arrives for `auth_openssh_certificate` (deferred(scope)). Not actionable work; decline is the standing disposition (ADR-004 territory).
|
External-trigger tracker for [OQ-04](docs/architecture/open-questions.md): does an OpenSSH-CA-trust use case ever arrive for `auth_openssh_certificate`? (deferred(scope)). Not actionable work; decline is the standing disposition (ADR-004 territory).
|
||||||
|
|
||||||
## Work
|
## Work
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: architecture/oq-09-env-request-allowlist
|
id: architecture/oq-09-env-request-allowlist
|
||||||
name: "OQ-09 tracker — env_request allowlist use case"
|
name: "OQ-09 tracker: env_request allowlist use case"
|
||||||
status: pending
|
status: pending
|
||||||
depends_on: []
|
depends_on: []
|
||||||
scope: single
|
scope: single
|
||||||
@@ -12,7 +12,7 @@ tags: [deferred-oq, external-trigger]
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
External-trigger tracker for [OQ-09](docs/architecture/open-questions.md) — whether a consuming assembly ever wants a wire-supplied env allowlist on session channels (deferred(scope)). Decline-by-default is standing posture.
|
External-trigger tracker for [OQ-09](docs/architecture/open-questions.md): whether a consuming assembly ever wants a wire-supplied env allowlist on session channels (deferred(scope)). Decline-by-default is standing posture.
|
||||||
|
|
||||||
## Work
|
## Work
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: architecture/oq-13-alkfs-sftp
|
id: architecture/oq-13-alkfs-sftp
|
||||||
name: "OQ-13 tracker — alkfs landed (sftp implementation unblocks)"
|
name: "OQ-13 tracker: alkfs landed, sftp implementation unblocks"
|
||||||
status: pending
|
status: pending
|
||||||
depends_on: []
|
depends_on: []
|
||||||
scope: single
|
scope: single
|
||||||
@@ -12,7 +12,7 @@ tags: [deferred-oq, external-trigger]
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
External-trigger tracker for [OQ-13](docs/architecture/open-questions.md) — the `sftp` feature's implementation waits for alkfs (family ADR-002, paused for alkforge). The `sftp` feature ships declining (tested decline contract on both directions) until this unblocks.
|
External-trigger tracker for [OQ-13](docs/architecture/open-questions.md): the `sftp` feature's implementation waits for alkfs (family ADR-002, paused for alkforge). The `sftp` feature ships declining (tested decline contract on both directions) until this unblocks.
|
||||||
|
|
||||||
## Work
|
## Work
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: review-wave-1
|
id: review-wave-1
|
||||||
name: Wave 1 gate — the foundations review
|
name: "Wave 1 gate: the foundations review"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w1-manifest, w1-error-model, w1-key-sources, w1-host-key-policy, w1-fingerprint, w1-config, w1-test-doubles]
|
depends_on: [w1-manifest, w1-error-model, w1-key-sources, w1-host-key-policy, w1-fingerprint, w1-config, w1-test-doubles]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -11,7 +11,7 @@ level: review
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
The wave-1 review gate — the cheap-place gate. The config types, the
|
The wave-1 review gate, the cheap-place gate of the plan. The config types, the
|
||||||
error taxonomy, and the key-source seam are one-way doors in practice:
|
error taxonomy, and the key-source seam are one-way doors in practice:
|
||||||
every later task implements against them, and the published crate
|
every later task implements against them, and the published crate
|
||||||
freezes them. A shape error found here costs a task revision; found
|
freezes them. A shape error found here costs a task revision; found
|
||||||
@@ -21,24 +21,24 @@ across both directions.
|
|||||||
The gate reads the landed surface against the architecture line by
|
The gate reads the landed surface against the architecture line by
|
||||||
line:
|
line:
|
||||||
|
|
||||||
- ADR-002 — the pin as declared (russh 0.65, `aws-lc-rs` only, no
|
- ADR-002, the pin as declared (russh 0.65, `aws-lc-rs` only, no
|
||||||
`flate2`/`rsa` in the tree, edition 2024, MSRV 1.89); the recorded
|
`flate2`/`rsa` in the tree, edition 2024, MSRV 1.89); the recorded
|
||||||
`ssh-key` RC exception verified in `cargo tree`.
|
`ssh-key` RC exception verified in `cargo tree`.
|
||||||
- ADR-003 — the feature graph shape (flat, independent, `default =
|
- ADR-003, the feature graph shape (flat, independent, `default =
|
||||||
[]`); the recorded interpretation (per-feature sibling deps land
|
[]`); the recorded interpretation (per-feature sibling deps land
|
||||||
with their first consuming wave) confirmed or corrected.
|
with their first consuming wave) confirmed or corrected.
|
||||||
- ADR-004 — the fingerprint format invariant (parity vectors green);
|
- ADR-004, the fingerprint format invariant (parity vectors green);
|
||||||
the `max_auth_attempts` default posture in `Limits`.
|
the `max_auth_attempts` default posture in `Limits`.
|
||||||
- ADR-005 — the custody chain (decrypt-at-use, `Zeroizing` wrap,
|
- ADR-005, the custody chain (decrypt-at-use, `Zeroizing` wrap,
|
||||||
distinct failure variants, `EncryptedData` opaque, host-key load
|
distinct failure variants, `EncryptedData` opaque, host-key load
|
||||||
path, plaintext never persists).
|
path, plaintext never persists).
|
||||||
- OQ-05 — the policy seam (fail-closed default, no accept-all
|
- OQ-05, the policy seam (fail-closed default, no accept-all
|
||||||
built-in, policy-blind crate).
|
built-in, policy-blind crate).
|
||||||
- OQ-08/09 — the serve-config `Option`-field semantics (unset →
|
- OQ-08/09, the serve-config `Option`-field semantics (unset →
|
||||||
decline, no crate default, no construction-time validation drift).
|
decline, no crate default, no construction-time validation drift).
|
||||||
- The error model — no catch-alls; auth declines are dispositions, not
|
- The error model: no catch-alls; auth declines are dispositions, not
|
||||||
errors.
|
errors.
|
||||||
- Docs honesty — module docs state what is deliberately not here yet
|
- Docs honesty, or that module docs state what is deliberately not here yet
|
||||||
(the stack, the mappings, the client direction).
|
(the stack, the mappings, the client direction).
|
||||||
|
|
||||||
Verdict: green (wave 2 decomposes, absorbing corrections) or findings
|
Verdict: green (wave 2 decomposes, absorbing corrections) or findings
|
||||||
@@ -66,23 +66,23 @@ booked (remediation before wave 2 decomposes).
|
|||||||
|
|
||||||
### Gate reading (line by line, 2026-10-28)
|
### Gate reading (line by line, 2026-10-28)
|
||||||
|
|
||||||
- **ADR-002** ✓ — `Cargo.toml` carries `russh = { version = "0.65",
|
- **ADR-002** ✓, because `Cargo.toml` carries `russh = { version = "0.65",
|
||||||
default-features = false, features = ["aws-lc-rs"] }`; edition 2024,
|
default-features = false, features = ["aws-lc-rs"] }`; edition 2024,
|
||||||
`rust-version = "1.89"`; `cargo tree -e normal` confirms **no
|
`rust-version = "1.89"`; `cargo tree -e normal` confirms **no
|
||||||
`rsa`, no `flate2`, no `ring`**, and the recorded
|
`rsa`, no `flate2`, no `ring`**, and the recorded
|
||||||
`ssh-key =0.7.0-rc.11` RC exception riding the russh keys path is
|
`ssh-key =0.7.0-rc.11` RC exception riding the russh keys path is
|
||||||
present exactly as ADR-002 records it.
|
present exactly as ADR-002 records it.
|
||||||
- **ADR-003** ✓ — `session`/`git`/`tunnel`/`sftp` declared, flat,
|
- **ADR-003** ✓, since `session`/`git`/`tunnel`/`sftp` declared, flat,
|
||||||
`default = []`, nothing implied; the per-feature sibling deps
|
`default = []`, nothing implied; the per-feature sibling deps
|
||||||
(`alktty`/`alktunnels`/`alkgit`) are **not** in the manifest — the
|
(`alktty`/`alktunnels`/`alkgit`) are **not** in the manifest; the
|
||||||
recorded interpretation (they land with their first consuming wave)
|
recorded interpretation (they land with their first consuming wave)
|
||||||
confirmed as recorded.
|
confirmed as recorded.
|
||||||
- **ADR-004** ✓ — `src/fingerprint.rs` is the single fingerprint path;
|
- **ADR-004** ✓, since `src/fingerprint.rs` is the single fingerprint path;
|
||||||
both parity suites (`tests/fingerprint_parity.rs`,
|
both parity suites (`tests/fingerprint_parity.rs`,
|
||||||
`tests/doubles.rs` fixture rows) are green against the
|
`tests/doubles.rs` fixture rows) are green against the
|
||||||
`ssh-keygen -lf`-recorded manifest; `Limits::max_auth_attempts`
|
`ssh-keygen -lf`-recorded manifest; `Limits::max_auth_attempts`
|
||||||
defaults to 10 with the decision-6 rationale in its doc.
|
defaults to 10 with the decision-6 rationale in its doc.
|
||||||
- **ADR-005** ✓ — the custody chain is in code: one
|
- **ADR-005** ✓, as the custody chain is in code: one
|
||||||
`vault.decrypt` → `Zeroizing<String>` wrap at the adapter seam →
|
`vault.decrypt` → `Zeroizing<String>` wrap at the adapter seam →
|
||||||
one-time `decode_secret_key`; the vault's taxonomy reshaped to the
|
one-time `decode_secret_key`; the vault's taxonomy reshaped to the
|
||||||
five distinct variants (no re-export, no collapse); `EncryptedData`
|
five distinct variants (no re-export, no collapse); `EncryptedData`
|
||||||
@@ -92,32 +92,33 @@ booked (remediation before wave 2 decomposes).
|
|||||||
no plaintext persistence anywhere. The known-gap (vault's plain
|
no plaintext persistence anywhere. The known-gap (vault's plain
|
||||||
`String` return) is documented as the ADR-005 decision-3 contract,
|
`String` return) is documented as the ADR-005 decision-3 contract,
|
||||||
not silently absorbed.
|
not silently absorbed.
|
||||||
- **OQ-05** ✓ — `HostKeyPolicy` seam with `RejectAll` fail-closed
|
- **OQ-05** ✓: the `HostKeyPolicy` seam offers a `RejectAll` fail-closed
|
||||||
default; **no accept-all built-in** (the permissive posture exists
|
default; **no accept-all built-in** (the permissive posture exists
|
||||||
only as the double's explicit `with_default(true)` choice in tests —
|
only as the double's explicit `with_default(true)` choice in tests,
|
||||||
correct posture); the crate is policy-blind.
|
which is the correct posture); the crate is policy-blind.
|
||||||
- **OQ-08/09** ✓ — `shell_argv`/`exec_wrapper_argv` are `Option` with
|
- **OQ-08/09** ✓: `shell_argv` and
|
||||||
|
`exec_wrapper_argv` are both `Option` with
|
||||||
unset→decline semantics in the type docs; no construction-time
|
unset→decline semantics in the type docs; no construction-time
|
||||||
validation, no config-error type (the recorded interpretation
|
validation, no config-error type (the recorded interpretation
|
||||||
confirmed — none was needed); `session_cwd`/`session_env` are
|
confirmed, and none was needed); `session_cwd`/`session_env` are
|
||||||
assembly-supplied, never wire-derived; `channel_client` absent →
|
assembly-supplied, never wire-derived; `channel_client` absent →
|
||||||
tunnel rows decline even under the feature.
|
tunnel rows decline even under the feature.
|
||||||
- **Error model** ✓ — five closed variants, no catch-all; the
|
- **Error model** ✓: five closed variants, no catch-all; the
|
||||||
dispositions-not-errors contract (auth declines, wire declines,
|
dispositions-not-errors contract (auth declines, wire declines,
|
||||||
config validation) is written into `error.rs`'s module docs.
|
config validation) is written into `error.rs`'s module docs.
|
||||||
- **Docs honesty** ✓ — every module's docs state what is deliberately
|
- **Docs honesty** ✓: every module's docs state what is deliberately
|
||||||
not here yet (russh `Config` construction and the dispatch are
|
not here yet (russh `Config` construction and the dispatch are
|
||||||
wave-2; dial wiring is wave-4; the grammar is wave-3).
|
wave-2; dial wiring is wave-4; the grammar is wave-3).
|
||||||
|
|
||||||
### Interpretations (flagged for this gate) — all confirmed
|
### Interpretations (flagged for this gate), all confirmed
|
||||||
|
|
||||||
1. Sibling-dep timing — confirmed (see ADR-003 above).
|
1. Sibling-dep timing: confirmed (see ADR-003 above).
|
||||||
2. Vault-double shape — confirmed: `TestVault` wraps alkvault 0.1's
|
2. Vault-double shape, confirmed: `TestVault` wraps alkvault 0.1's
|
||||||
own in-memory construction (`VaultServiceHandle::new()` +
|
own in-memory construction (`VaultServiceHandle::new()` +
|
||||||
`unlock_new`) rather than re-implementing behind the source trait,
|
`unlock_new`) rather than re-implementing behind the source trait,
|
||||||
so the real error taxonomy and GCM behavior ride every row (the
|
so the real error taxonomy and GCM behavior ride every row (the
|
||||||
decision is recorded in `tests/common/mod.rs`'s module docs).
|
decision is recorded in `tests/common/mod.rs`'s module docs).
|
||||||
3. Policy verdict type — confirmed: accept/reject `bool`, pinned to
|
3. Policy verdict type, confirmed: accept/reject `bool`, pinned to
|
||||||
the russh 0.65 `check_server_key → Result<bool, E>` hook shape,
|
the russh 0.65 `check_server_key → Result<bool, E>` hook shape,
|
||||||
with the `Err` channel held for wire/IO failures (recorded in
|
with the `Err` channel held for wire/IO failures (recorded in
|
||||||
`host_key.rs`'s module docs).
|
`host_key.rs`'s module docs).
|
||||||
@@ -127,21 +128,21 @@ booked (remediation before wave 2 decomposes).
|
|||||||
Small doc/polish findings; each is a one-to-few-line fix:
|
Small doc/polish findings; each is a one-to-few-line fix:
|
||||||
|
|
||||||
- `cargo doc --no-deps` emitted 2 rustdoc warnings (ambiguous
|
- `cargo doc --no-deps` emitted 2 rustdoc warnings (ambiguous
|
||||||
`[`async_trait`]` intra-doc links — crate and attribute-macro share
|
`[`async_trait`]` intra-doc links, since the crate and the attribute-macro share
|
||||||
the name) in `key_source.rs` and `host_key.rs` → resolved as plain
|
the name) in `key_source.rs` and `host_key.rs` → resolved as plain
|
||||||
code spans. The crates' verification set does not run rustdoc, so
|
code spans. The crates' verification set does not run rustdoc, so
|
||||||
these had gone unnoticed — worth remembering at wave 5's
|
these had gone unnoticed, which is worth remembering at wave 5's
|
||||||
"public-API rustdoc warning-free" gate.
|
"public-API rustdoc warning-free" gate.
|
||||||
- `key_source.rs` `HostKeySource` docs: stray `)` typo ("on host
|
- `key_source.rs` `HostKeySource` docs: stray `)` typo ("on host
|
||||||
*pin*) trust").
|
*pin*) trust").
|
||||||
- `lib.rs` crate docs: stale line ("the dependency surface and
|
- `lib.rs` crate docs: stale line ("the dependency surface and
|
||||||
feature graph land with implementation start") — now landed;
|
feature graph land with implementation start") is now landed;
|
||||||
rewritten to state the landed/future split honestly.
|
rewritten to state the landed/future split honestly.
|
||||||
- Two redundant test lines removed (a discarded pre-probe
|
- Two redundant test lines removed (a discarded pre-probe
|
||||||
`try_sign` in `key_source.rs`'s usability helper; a redundant
|
`try_sign` in `key_source.rs`'s usability helper; a redundant
|
||||||
`None` re-assignment in `config.rs`'s independent-flip row).
|
`None` re-assignment in `config.rs`'s independent-flip row).
|
||||||
|
|
||||||
Nothing found rose to the level of a booked finding — no shape error
|
Nothing found rose to the level of a booked finding, and no shape error
|
||||||
in the config types, the error taxonomy, or the key-source seam.
|
in the config types, the error taxonomy, or the key-source seam.
|
||||||
|
|
||||||
### Verification (all green)
|
### Verification (all green)
|
||||||
@@ -159,9 +160,9 @@ in the config types, the error taxonomy, or the key-source seam.
|
|||||||
ADR-002/003/004/005 and the OQ-05/08/09 resolutions, line by line;
|
ADR-002/003/004/005 and the OQ-05/08/09 resolutions, line by line;
|
||||||
all three recorded interpretations (sibling-dep timing, vault-double
|
all three recorded interpretations (sibling-dep timing, vault-double
|
||||||
shape, policy verdict type) were confirmed as recorded. Only minor
|
shape, policy verdict type) were confirmed as recorded. Only minor
|
||||||
doc/polish findings surfaced — fixed inline (rustdoc link
|
doc/polish findings surfaced, and they were fixed inline (rustdoc link
|
||||||
disambiguation, a doc typo, a stale crate-doc line, two redundant
|
disambiguation, a doc typo, a stale crate-doc line, two redundant
|
||||||
test lines), recorded above. No shape errors in the one-way-door
|
test lines), recorded above. No shape errors in the one-way-door
|
||||||
surfaces. Wave 2 decomposition is unblocked; it should absorb the
|
surfaces. Wave 2 decomposition is unblocked; it should absorb the
|
||||||
rustdoc-hygiene lesson (run `cargo doc --no-deps` on gates) — no fix
|
rustdoc-hygiene lesson (run `cargo doc --no-deps` on gates), though no fix
|
||||||
tasks needed.
|
tasks are needed.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: review-wave-2
|
id: review-wave-2
|
||||||
name: Wave 2 gate — the server-stack review
|
name: "Wave 2 gate: the server-stack review"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w2-server-mount, w2-auth-bridge, w2-loopback-harness, w2-dispatch-decline-core, w2-spawn-return, w2-bounded-resources]
|
depends_on: [w2-server-mount, w2-auth-bridge, w2-loopback-harness, w2-dispatch-decline-core, w2-spawn-return, w2-bounded-resources]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -13,39 +13,39 @@ level: review
|
|||||||
|
|
||||||
The wave-2 review gate. The server stack is the substrate every
|
The wave-2 review gate. The server stack is the substrate every
|
||||||
mapping composes into (wave 3 hangs three mappings and the sftp row on
|
mapping composes into (wave 3 hangs three mappings and the sftp row on
|
||||||
it; wave 4 shares its foundations) — a shape error in the mount
|
it; wave 4 shares its foundations), so a shape error in the mount
|
||||||
signature, the auth-bridge state, or the dispatch routing costs churn
|
signature, the auth-bridge state, or the dispatch routing costs churn
|
||||||
across all of them. The gate reads the landed stack against the
|
across all of them. The gate reads the landed stack against the
|
||||||
architecture line by line:
|
architecture line by line:
|
||||||
|
|
||||||
- **ADR-001** — the mount contract: `SshAdapter::handle` takes a
|
- **ADR-001**, the mount contract: `SshAdapter::handle` takes a
|
||||||
generic stream, mounts the full russh stack per stream, edge
|
generic stream, mounts the full russh stack per stream, edge
|
||||||
termination only; no ALPN/relay code exists; the per-stream limits
|
termination only; no ALPN/relay code exists; the per-stream limits
|
||||||
posture holds.
|
posture holds.
|
||||||
- **ADR-002** — the Config construction (honest advertisement
|
- **ADR-002**, the Config construction (honest advertisement
|
||||||
`{publickey}` exactly; the knob mapping; no `des`/`dsa` posture
|
`{publickey}` exactly; the knob mapping; no `des`/`dsa` posture
|
||||||
drift; the pin untouched).
|
drift; the pin untouched).
|
||||||
- **ADR-003** — the feature gates as the dispatch's first `cfg`
|
- **ADR-003**, the feature gates as the dispatch's first `cfg`
|
||||||
consumers: flat, independent, `default = []` declines everything;
|
consumers: flat, independent, `default = []` declines everything;
|
||||||
x11/auth-agent never feature-gated; honest advertisement per build.
|
x11/auth-agent never feature-gated; honest advertisement per build.
|
||||||
- **ADR-004** — the dispositions in code (explicit declines for every
|
- **ADR-004**, the dispositions in code (explicit declines for every
|
||||||
non-publickey method; offered-hook accept-and-delegate; ed25519-only
|
non-publickey method; offered-hook accept-and-delegate; ed25519-only
|
||||||
explicit decline; fingerprint-only resolution; the identity rides
|
explicit decline; fingerprint-only resolution; the identity rides
|
||||||
the session).
|
the session).
|
||||||
- **ADR-006** — parse-only dispatch (no detect residue); decline-first-
|
- **ADR-006**, parse-only dispatch (no detect residue); decline-first-
|
||||||
alloc-never structurally; **the spawn-and-return constraint pinned
|
alloc-never structurally; **the spawn-and-return constraint pinned
|
||||||
by test, not convention**.
|
by test, not convention**.
|
||||||
- **`mapping.md`** — the skeleton decline-matrix rows green per build
|
- **`mapping.md`**, the skeleton decline-matrix rows green per build
|
||||||
through the harness; the wire decline reasons vanilla-observable.
|
through the harness; the wire decline reasons vanilla-observable.
|
||||||
- **Bounded resources** — the cap enforced and recovering; the knobs
|
- **Bounded resources**, the cap enforced and recovering; the knobs
|
||||||
behaviorally verified or recorded; no unbounded shape anywhere.
|
behaviorally verified or recorded; no unbounded shape anywhere.
|
||||||
- **The harness** — the contract-test vehicle convention established;
|
- **The harness**, with the contract-test vehicle convention established;
|
||||||
the wave-1 doubles exercised live.
|
the wave-1 doubles exercised live.
|
||||||
- **Docs honesty** — module docs state what is deliberately not here
|
- **Docs honesty**: module docs state what is deliberately not here
|
||||||
yet (the family seams, the client direction); the skeleton-stub
|
yet (the family seams, the client direction); the skeleton-stub
|
||||||
behavior (declining routes awaiting wave 3) documented as such.
|
behavior (declining routes awaiting wave 3) documented as such.
|
||||||
- **The wave-1 gate's lesson absorbed** — `cargo doc --no-deps` runs
|
- **The wave-1 gate's lesson absorbed**, with `cargo doc --no-deps`
|
||||||
clean on this gate too.
|
running clean on this gate too.
|
||||||
|
|
||||||
Verdict: green (wave 3 decomposes, absorbing corrections) or findings
|
Verdict: green (wave 3 decomposes, absorbing corrections) or findings
|
||||||
booked (remediation before wave 3 decomposes).
|
booked (remediation before wave 3 decomposes).
|
||||||
@@ -71,16 +71,16 @@ booked (remediation before wave 3 decomposes).
|
|||||||
|
|
||||||
### Gate reading (line by line, 2026-10-28; commits b90e89b…196289f)
|
### Gate reading (line by line, 2026-10-28; commits b90e89b…196289f)
|
||||||
|
|
||||||
- **ADR-001** ✓ — `SshAdapter::handle` is the mount: generic
|
- **ADR-001** ✓, as `SshAdapter::handle` is the mount: generic
|
||||||
`AsyncRead + AsyncWrite` stream in, the full russh stack per stream
|
`AsyncRead + AsyncWrite` stream in, the full russh stack per stream
|
||||||
via `run_stream`, returned **run to completion**
|
via `run_stream`, returned **run to completion**
|
||||||
(`Result<(), AdapterError>` — the decision recorded in the task's
|
(`Result<(), AdapterError>`, a decision recorded in the task's
|
||||||
notes; no russh type rides the signature). No ALPN code, no relay
|
notes; no russh type rides the signature). No ALPN code, no relay
|
||||||
leg, no `alk/ssh` string anywhere in `src/`. The per-stream limits
|
leg, no `alk/ssh` string anywhere in `src/`. The per-stream limits
|
||||||
posture holds: each mounted stream builds its own `SessionHandler`
|
posture holds: each mounted stream builds its own `SessionHandler`
|
||||||
over the serve-config, so the cap/timeout/window budget is
|
over the serve-config, so the cap/timeout/window budget is
|
||||||
per-stream exactly as ADR-001's neutral consequence states.
|
per-stream exactly as ADR-001's neutral consequence states.
|
||||||
- **ADR-002** ✓ — the `Config` construction maps `Limits` one-for-one
|
- **ADR-002** ✓, as the `Config` construction maps `Limits` one-for-one
|
||||||
(`window_size` / `maximum_packet_size` / `channel_buffer_size` /
|
(`window_size` / `maximum_packet_size` / `channel_buffer_size` /
|
||||||
`max_auth_attempts` verbatim; `inactivity_timeout` **always
|
`max_auth_attempts` verbatim; `inactivity_timeout` **always
|
||||||
`Some`** with a test pinning the default postures and a second
|
`Some`** with a test pinning the default postures and a second
|
||||||
@@ -91,7 +91,7 @@ booked (remediation before wave 3 decomposes).
|
|||||||
`aws-lc-rs`, no `flate2`/`rsa`/`des`/`dsa`); the recorded
|
`aws-lc-rs`, no `flate2`/`rsa`/`des`/`dsa`); the recorded
|
||||||
deliberations (`server_id` stays russh-default; keepalive stays
|
deliberations (`server_id` stays russh-default; keepalive stays
|
||||||
client-direction) are in the task's notes.
|
client-direction) are in the task's notes.
|
||||||
- **ADR-003** ✓ — the wave-2 dispatch core is the feature graph's
|
- **ADR-003** ✓, since the wave-2 dispatch core is the feature graph's
|
||||||
first `cfg` consumer: flat, independent gates read via `cfg!`
|
first `cfg` consumer: flat, independent gates read via `cfg!`
|
||||||
predicates (`session ∨ git` for session channels;
|
predicates (`session ∨ git` for session channels;
|
||||||
`tunnel` for the three tunnel opens; `session` for pty/shell/pipe
|
`tunnel` for the three tunnel opens; `session` for pty/shell/pipe
|
||||||
@@ -100,36 +100,36 @@ booked (remediation before wave 3 decomposes).
|
|||||||
forwarding decline unconditionally, never feature-gated; the `sftp`
|
forwarding decline unconditionally, never feature-gated; the `sftp`
|
||||||
feature gates nothing and the subsystem row declines in every
|
feature gates nothing and the subsystem row declines in every
|
||||||
build (OQ-13 placeholder).
|
build (OQ-13 placeholder).
|
||||||
- **ADR-004** ✓ — all dispositions in code as explicit hook
|
- **ADR-004** ✓, with all dispositions in code as explicit hook
|
||||||
implementations: `auth_publickey` (ed25519-only gate checked on
|
implementations: `auth_publickey` (ed25519-only gate checked on
|
||||||
**both** the signature algorithm and the key's algorithm before any
|
**both** the signature algorithm and the key's algorithm before any
|
||||||
resolution → fingerprint → `resolve_from_fingerprint` →
|
resolution → fingerprint → `resolve_from_fingerprint` →
|
||||||
accept-with-identity / reject fail-closed), the offered hook
|
accept-with-identity / reject fail-closed), the offered hook
|
||||||
accept-and-delegates, and the four non-publickey methods each have
|
accept-and-delegates, and the four non-publickey methods each have
|
||||||
an explicit reject body (never inherited). The bridge core
|
an explicit reject body (never inherited). The bridge core
|
||||||
(`resolve_bridge`) takes no username parameter — OQ-03 holds
|
(`resolve_bridge`) takes no username parameter, so OQ-03 holds
|
||||||
structurally; wire rows prove username-independence. Auth declines
|
structurally; wire rows prove username-independence. Auth declines
|
||||||
are `Auth::Reject` dispositions end to end. The identity lands in
|
are `Auth::Reject` dispositions end to end. The identity lands in
|
||||||
per-connection handler state
|
per-connection handler state
|
||||||
(`SessionHandler::resolved_identity`); its *session-level landing*
|
(`SessionHandler::resolved_identity`); its *session-level landing*
|
||||||
is wave-3 assertion territory (flagged in the task's notes — code
|
is wave-3 assertion territory (flagged in the task's notes, as code
|
||||||
verifiable only through `auth_succeeded` consumption), confirmed as
|
is verifiable only through `auth_succeeded` consumption), confirmed as
|
||||||
correctly deferred.
|
correctly deferred.
|
||||||
- **ADR-006** ✓ — parse-only dispatch with no detect residue (the
|
- **ADR-006** ✓, showing parse-only dispatch with no detect residue (the
|
||||||
SOCKS-detect is gone; a request-less session channel serves
|
SOCKS-detect is gone; a request-less session channel serves
|
||||||
nothing by construction — no serving path exists outside the
|
nothing by construction, since no serving path exists outside the
|
||||||
request hooks); decline-first-alloc-never verified structurally:
|
request hooks); decline-first-alloc-never verified structurally:
|
||||||
every open-level decline answers before any accept; the only
|
every open-level decline answers before any accept; the only
|
||||||
pre-decline resource on any request path is none (exec parses via a
|
pre-decline resource on any request path is none (exec parses via a
|
||||||
pure fn first), and the spawned serving paths' only allocation is
|
pure fn first), and the spawned serving paths' only allocation is
|
||||||
the post-decision handle clone; spawn-and-return is **pinned by
|
the post-decision handle clone; spawn-and-return is **pinned by
|
||||||
test, not convention** — the `serve.rs` unit row (spawn returns
|
test, not convention**; the `serve.rs` unit row (spawn returns
|
||||||
while the path is pending) plus the two wire rows
|
while the path is pending) plus the two wire rows
|
||||||
(`tests/spawn_return.rs`: handler-returns-fast under follow-up
|
(`tests/spawn_return.rs`: handler-returns-fast under follow-up
|
||||||
traffic with the spawn still gated; exit-status strictly before
|
traffic with the spawn still gated; exit-status strictly before
|
||||||
close, timestamps proving the report outlived the handler's
|
close, timestamps proving the report outlived the handler's
|
||||||
return).
|
return).
|
||||||
- **mapping.md** ✓ — the skeleton decline-matrix rows are green per
|
- **mapping.md** ✓, with the skeleton decline-matrix rows green per
|
||||||
build through the harness
|
build through the harness
|
||||||
(`tests/dispatch_core.rs`: shared rows in every build; per-build
|
(`tests/dispatch_core.rs`: shared rows in every build; per-build
|
||||||
modules cfg-pinned to exactly one feature combination so no
|
modules cfg-pinned to exactly one feature combination so no
|
||||||
@@ -139,7 +139,7 @@ booked (remediation before wave 3 decomposes).
|
|||||||
(`AdministrativelyProhibited` / the tunnel stub's `ConnectFailed`),
|
(`AdministrativelyProhibited` / the tunnel stub's `ConnectFailed`),
|
||||||
request-level capability declines answer `channel_failure` with
|
request-level capability declines answer `channel_failure` with
|
||||||
the channel provably still usable.
|
the channel provably still usable.
|
||||||
- **Bounded resources** ✓ — the in-crate cap is enforced at the
|
- **Bounded resources** ✓, with the in-crate cap enforced at the
|
||||||
handler surface, recovering on close (wire row: cap reached →
|
handler surface, recovering on close (wire row: cap reached →
|
||||||
decline; close → open succeeds again, deterministic on the FIFO
|
decline; close → open succeeds again, deterministic on the FIFO
|
||||||
wire ordering; zero-cap row refuses everything without poisoning);
|
wire ordering; zero-cap row refuses everything without poisoning);
|
||||||
@@ -152,49 +152,49 @@ booked (remediation before wave 3 decomposes).
|
|||||||
(wave-3 grammar's consumption, per the plan bullet). No unbounded
|
(wave-3 grammar's consumption, per the plan bullet). No unbounded
|
||||||
shape anywhere: accepted-but-unserved session channels hold bounded
|
shape anywhere: accepted-but-unserved session channels hold bounded
|
||||||
buffers only (window/buffer knobs).
|
buffers only (window/buffer knobs).
|
||||||
- **The harness** ✓ — the contract-test vehicle convention
|
- **The harness** ✓, with the contract-test vehicle convention
|
||||||
established exactly as the task pinned it: tokio-duplex transport
|
established exactly as the task pinned it: tokio-duplex transport
|
||||||
+ `connect_stream` (decision recorded in the module docs, over TCP
|
+ `connect_stream` (decision recorded in the module docs, over TCP
|
||||||
loopback), sibling-of-the-doubles placement under `tests/common/`,
|
loopback), sibling-of-the-doubles placement under `tests/common/`,
|
||||||
the `drive` vocabulary the wave-3 mappings inherit, and the wave-1
|
the `drive` vocabulary the wave-3 mappings inherit, and the wave-1
|
||||||
doubles exercised live through it (`MemIdentityProvider`,
|
doubles exercised live through it (`MemIdentityProvider`,
|
||||||
`MemHostKeySource`, `MemHostKeyPolicy` both verdicts, fixture
|
`MemHostKeySource`, `MemHostKeyPolicy` both verdicts, fixture
|
||||||
plumbing) — the acceptance row
|
plumbing), as the acceptance row
|
||||||
(`in_process_pair_connects_and_authenticates_end_to_end`) plus the
|
(`in_process_pair_connects_and_authenticates_end_to_end`) plus the
|
||||||
auth-bridge and dispatch rows all ride it.
|
auth-bridge and dispatch rows all ride it.
|
||||||
- **Docs honesty** ✓ — the handler module docs state precisely what
|
- **Docs honesty** ✓, as the handler module docs state precisely what
|
||||||
is deliberately not here (the family seams, the client direction,
|
is deliberately not here (the family seams, the client direction,
|
||||||
the store-and-take handoff, the wave-3 tunnel accept-flow), each
|
the store-and-take handoff, the wave-3 tunnel accept-flow), each
|
||||||
seam stub documents its wave-3 replacement point, and the
|
seam stub documents its wave-3 replacement point, and the
|
||||||
inherited-posture note (the bare `auth-agent` /
|
inherited-posture note (the bare `auth-agent` /
|
||||||
`forwarded-streamlocal` channel types decline via russh's own
|
`forwarded-streamlocal` channel types decline via russh's own
|
||||||
inline rejection at this pin — flagged as an upstream watch item, a
|
inline rejection at this pin, flagged as an upstream watch item since a
|
||||||
pin bump that changed it would surface as accepted-then-dropped) is
|
pin bump that changed it would surface as accepted-then-dropped) is
|
||||||
recorded rather than silently relied on.
|
recorded rather than silently relied on.
|
||||||
- **The wave-1 gate's lesson absorbed** ✓ — `cargo doc --no-deps`
|
- **The wave-1 gate's lesson absorbed** ✓, as `cargo doc --no-deps`
|
||||||
ran clean on this gate (0 warnings), and `taskgraph validate` passes.
|
ran clean on this gate (0 warnings), and `taskgraph validate` passes.
|
||||||
|
|
||||||
### Interpretations (flagged for this gate) — all confirmed
|
### Interpretations (flagged for this gate), all confirmed
|
||||||
|
|
||||||
1. **Git-parseability probe is skeleton-level** — confirmed:
|
1. **Git-parseability probe is skeleton-level**, confirmed:
|
||||||
`classify_exec` carries service-prefix + path-shape only, its
|
`classify_exec` carries service-prefix + path-shape only, its
|
||||||
module docs name the wave-3 replacement explicitly, and the probe
|
module docs name the wave-3 replacement explicitly, and the probe
|
||||||
rows pin exactly the POC-3 stage-A facts the skeleton needs
|
rows pin exactly the POC-3 stage-A facts the skeleton needs
|
||||||
(unknown service → pipe row; total over arbitrary input).
|
(unknown service → pipe row; total over arbitrary input).
|
||||||
2. **Routing stubs decline at the family-seam boundary, documented as
|
2. **Routing stubs decline at the family-seam boundary, documented as
|
||||||
skeleton** — confirmed: the stub bodies are the named wave-3
|
skeleton**; confirmed: the stub bodies are the named wave-3
|
||||||
replacement points (`git_row_stub` … `tunnel_row_stub`), the
|
replacement points (`git_row_stub` … `tunnel_row_stub`), the
|
||||||
serving-path decline shape (failure then close) is wire-pinned,
|
serving-path decline shape (failure then close) is wire-pinned,
|
||||||
and the tunnel stub's `ConnectFailed` reason matches ADR-006
|
and the tunnel stub's `ConnectFailed` reason matches ADR-006
|
||||||
decision 1's fail shape.
|
decision 1's fail shape.
|
||||||
3. **Adapter error surface shaped in `w2-server-mount`** — confirmed:
|
3. **Adapter error surface shaped in `w2-server-mount`**, confirmed:
|
||||||
`AdapterError` is the closed two-class taxonomy
|
`AdapterError` is the closed two-class taxonomy
|
||||||
(`Construction(#[from] KeySourceError)` + `Wire(#[from]
|
(`Construction(#[from] KeySourceError)` + `Wire(#[from]
|
||||||
russh::Error)`) with the locked→no-server rule at construction and
|
russh::Error)`) with the locked→no-server rule at construction and
|
||||||
no catch-all; the wire decline reasons landed in the dispatch, not
|
no catch-all; the wire decline reasons landed in the dispatch, not
|
||||||
in the error taxonomy (the wave-1 reservation spent correctly).
|
in the error taxonomy (the wave-1 reservation spent correctly).
|
||||||
4. **Harness transport and placement pinned in
|
4. **Harness transport and placement pinned in
|
||||||
`w2-loopback-harness`** — confirmed, recorded in its module docs.
|
`w2-loopback-harness`**, confirmed and recorded in its module docs.
|
||||||
|
|
||||||
### Minor issues found and fixed inline (this session)
|
### Minor issues found and fixed inline (this session)
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ Doc-hygiene only, in the task files (no `src/` change warranted):
|
|||||||
|
|
||||||
- **No observability instrumentation on the decline paths.** The
|
- **No observability instrumentation on the decline paths.** The
|
||||||
request-level decline class's "reason" is currently only the
|
request-level decline class's "reason" is currently only the
|
||||||
routing decision documented per build — nothing server-side logs
|
routing decision documented per build, so nothing server-side logs
|
||||||
which reason a given wire decline carried (`tracing` is in the
|
which reason a given wire decline carried (`tracing` is in the
|
||||||
manifest per the ADR-002 toolkit but unused in `src/` so far). Small
|
manifest per the ADR-002 toolkit but unused in `src/` so far). Small
|
||||||
follow-up task for the next wave's decomposition (alongside the
|
follow-up task for the next wave's decomposition (alongside the
|
||||||
@@ -228,7 +228,7 @@ Doc-hygiene only, in the task files (no `src/` change warranted):
|
|||||||
flow.
|
flow.
|
||||||
|
|
||||||
No shape errors found in the mount signature, the auth-bridge state,
|
No shape errors found in the mount signature, the auth-bridge state,
|
||||||
or the dispatch routing — the three churn-critical surfaces this gate
|
or the dispatch routing, the three churn-critical surfaces this gate
|
||||||
exists to protect.
|
exists to protect.
|
||||||
|
|
||||||
### Verification (all green)
|
### Verification (all green)
|
||||||
@@ -253,7 +253,7 @@ everything, x11/auth-agent never gated), ADR-004's dispositions as
|
|||||||
explicit hook code (ed25519-only fingerprint bridge, four explicit
|
explicit hook code (ed25519-only fingerprint bridge, four explicit
|
||||||
declines, username structurally excluded), ADR-006's constraints
|
declines, username structurally excluded), ADR-006's constraints
|
||||||
parse-only dispatch, decline-first-alloc-never structural, and
|
parse-only dispatch, decline-first-alloc-never structural, and
|
||||||
spawn-and-return pinned by test over the real wire — plus the bounded
|
spawn-and-return pinned by test over the real wire, plus the bounded
|
||||||
in-crate channel cap enforced and recovering at the handler surface,
|
in-crate channel cap enforced and recovering at the handler surface,
|
||||||
the skeleton decline-matrix rows green per feature build through the
|
the skeleton decline-matrix rows green per feature build through the
|
||||||
loopback harness, and docs that state what is deliberately deferred.
|
loopback harness, and docs that state what is deliberately deferred.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: review-wave-3
|
id: review-wave-3
|
||||||
name: Wave 3 gate — the server-mappings review
|
name: "Wave 3 gate: the server-mappings review"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w3-seam-config, w3-decline-tracing, w3-git-mapping, w3-tty-session, w3-tunnel-direct, w3-tunnel-listen]
|
depends_on: [w3-seam-config, w3-decline-tracing, w3-git-mapping, w3-tty-session, w3-tunnel-direct, w3-tunnel-listen]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -12,53 +12,53 @@ level: review
|
|||||||
## Description
|
## Description
|
||||||
|
|
||||||
The wave-3 review gate. The server mappings are the crate's spine
|
The wave-3 review gate. The server mappings are the crate's spine
|
||||||
landed in wire-capable form — the wave-4 client direction reuses the
|
landed in wire-capable form, and the wave-4 client direction reuses the
|
||||||
same grammar/rows pointed upstream, wave 5 builds the full matrix on
|
same grammar/rows pointed upstream, wave 5 builds the full matrix on
|
||||||
these rows, and the published crate freezes this surface. The gate
|
these rows, and the published crate freezes this surface. The gate
|
||||||
reads the landed mappings against the architecture line by line:
|
reads the landed mappings against the architecture line by line:
|
||||||
|
|
||||||
- **mapping.md line-by-line** — each of the spine's server rows
|
- **mapping.md line-by-line**, covering each of the spine's server rows
|
||||||
(session/pty/shell/pipe; git exec; direct-tcpip;
|
(session/pty/shell/pipe; git exec; direct-tcpip;
|
||||||
direct-streamlocal; forwarded-tcpip; the global listen rows; the
|
direct-streamlocal; forwarded-tcpip; the global listen rows; the
|
||||||
request-level table) read against the landed code; the §exit
|
request-level table) read against the landed code; the §exit
|
||||||
derivation contract rules 1–6 verified in code + rows; the decline
|
derivation contract rules 1–6 verified in code + rows; the decline
|
||||||
matrix green **per feature build** through the harness (including
|
matrix green **per feature build** through the harness (including
|
||||||
the unset-field rows — feature on, handle unset).
|
the unset-field rows, with the feature on and the handle unset).
|
||||||
- **ADR-006** — route-1 bridge (accept only after the fabric open
|
- **ADR-006**, route-1 bridge (accept only after the fabric open
|
||||||
succeeds; `ConnectFailed` on failure), exec-only git (no subsystem
|
succeeds; `ConnectFailed` on failure), exec-only git (no subsystem
|
||||||
dispatch anywhere in the grammar), parse-only dispatch still
|
dispatch anywhere in the grammar), parse-only dispatch still
|
||||||
structural (no request-less serving), spawn-and-return over the
|
structural (no request-less serving), spawn-and-return over the
|
||||||
**real** serving paths (the wave-2 constraint re-pinned over
|
**real** serving paths (the wave-2 constraint re-pinned over
|
||||||
mappings, not stubs).
|
mappings, not stubs).
|
||||||
- **ADR-003** — the per-build matrices; honest advertisement
|
- **ADR-003**, the per-build matrices; honest advertisement
|
||||||
unchanged (an unfeatured type declines; the sftp feature still
|
unchanged (an unfeatured type declines; the sftp feature still
|
||||||
gates nothing, its rows still decline in every build — OQ-13);
|
gates nothing, its rows still decline in every build, per OQ-13);
|
||||||
x11/auth-agent never-feature rows untouched.
|
x11/auth-agent never-feature rows untouched.
|
||||||
- **Auth.md / identity** — the identity ride observable at every seam
|
- **Auth.md / identity**, the identity ride observable at every seam
|
||||||
(alktty/alkgit params; the channels connection for tunnels),
|
(alktty/alkgit params; the channels connection for tunnels),
|
||||||
`set_identity` once-only mechanics recorded; the parked wave-2
|
`set_identity` once-only mechanics recorded; the parked wave-2
|
||||||
`auth_succeeded` item verified discharged; no username anywhere.
|
`auth_succeeded` item verified discharged; no username anywhere.
|
||||||
- **OQ resolutions in code** — OQ-08 (unset-declines on shell/wrapper
|
- **OQ resolutions in code**, namely OQ-08 (unset-declines on shell/wrapper
|
||||||
rows), OQ-09 (env declined), OQ-10 (derivation rules + the
|
rows), OQ-09 (env declined), OQ-10 (derivation rules + the
|
||||||
PTY-path fidelity limit documented), OQ-11 (runtime-Unix gates
|
PTY-path fidelity limit documented), OQ-11 (runtime-Unix gates
|
||||||
compile-everywhere + clean off-Unix declines), OQ-13 (sftp rows
|
compile-everywhere + clean off-Unix declines), OQ-13 (sftp rows
|
||||||
unchanged).
|
unchanged).
|
||||||
- **Bounded resources** — no new unbounded shape: the cap across the
|
- **Bounded resources**, with no new unbounded shape: the cap across the
|
||||||
fabric window (release-on-failure pinned), the listener bookkeeping
|
fabric window (release-on-failure pinned), the listener bookkeeping
|
||||||
bounded, the control-row surfaces bounded, the bridge adapters
|
bounded, the control-row surfaces bounded, the bridge adapters
|
||||||
bounded.
|
bounded.
|
||||||
- **Tracing** — the uniform event vocabulary across all landed
|
- **Tracing**, the uniform event vocabulary across all landed
|
||||||
serve/decline paths; the safety posture held (no secrets, no
|
serve/decline paths; the safety posture held (no secrets, no
|
||||||
username).
|
username).
|
||||||
- **The smokes** — the three real-tool postures recorded honestly
|
- **The smokes**, with the three real-tool postures recorded honestly
|
||||||
(required/conditionally-skipped with reason); nothing silently
|
(required/conditionally-skipped with reason); nothing silently
|
||||||
green.
|
green.
|
||||||
- **Wave-2 gate's booked items** — every one discharged or explicitly
|
- **Wave-2 gate's booked items**: every one discharged or explicitly
|
||||||
re-booked (decline tracing = landed; acceptor vocabulary = landed
|
re-booked (decline tracing = landed; acceptor vocabulary = landed
|
||||||
in `w3-tunnel-listen`; identity landing = discharged; the
|
in `w3-tunnel-listen`; identity landing = discharged; the
|
||||||
inherited-auth-agent-open watch item = unchanged or re-read at any
|
inherited-auth-agent-open watch item = unchanged or re-read at any
|
||||||
pin touch).
|
pin touch).
|
||||||
- **Docs honesty** — module docs name what each replaced stub now
|
- **Docs honesty**: module docs name what each replaced stub now
|
||||||
is, what remains wave-4/5 material, and any new upstream notes
|
is, what remains wave-4/5 material, and any new upstream notes
|
||||||
(portable-pty's PTY-path exit shape; the `take_call_connection`
|
(portable-pty's PTY-path exit shape; the `take_call_connection`
|
||||||
semantics reading).
|
semantics reading).
|
||||||
@@ -84,103 +84,102 @@ booked (remediation before wave 4 decomposes).
|
|||||||
- docs/plans/implementation.md (wave 3 section + this gate's role)
|
- docs/plans/implementation.md (wave 3 section + this gate's role)
|
||||||
- docs/architecture/mapping.md, auth.md, client.md (the mirror),
|
- docs/architecture/mapping.md, auth.md, client.md (the mirror),
|
||||||
overview.md, open-questions.md, ADRs 002/003/004/005/006
|
overview.md, open-questions.md, ADRs 002/003/004/005/006
|
||||||
- docs/research/poc-3-mapping-bakeoff.md (stages A/C/D — the evidence
|
- docs/research/poc-3-mapping-bakeoff.md (stages A/C/D, the evidence
|
||||||
the contract tests re-land)
|
the contract tests re-land)
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
### Gate reading (line by line, 2026-10-29; commits 270365e…3d0253d)
|
### Gate reading (line by line, 2026-10-29; commits 270365e…3d0253d)
|
||||||
|
|
||||||
- **mapping.md line-by-line** ✓ — each spine row read against the
|
- **mapping.md line-by-line** ✓, each spine row read against the
|
||||||
landed code: the session/pty/shell/pipe rows (translation bridge,
|
landed code: the session/pty/shell/pipe rows (translation bridge,
|
||||||
OQ-08 argv conventions, pipe-less shell serves pipe-mode), the git
|
OQ-08 argv conventions, pipe-less shell serves pipe-mode), the git
|
||||||
exec row (full grammar, no-oracle validation, alkgit dispatch), the
|
exec row (full grammar, no-oracle validation, alkgit dispatch), the
|
||||||
direct-tcpip / direct-streamlocal rows (route-1 bridge), the
|
direct-tcpip / direct-streamlocal rows (route-1 bridge), the
|
||||||
forwarded-tcpip row (echo args + bound port, teardown-at-birth on
|
forwarded-tcpip row (echo args + bound port, teardown-at-birth on
|
||||||
correlation miss), the global listen rows (verbatim resource +
|
correlation miss), the global listen rows (verbatim resource +
|
||||||
bound-port out-param — one doc correction landed: §The listen dial
|
bound-port out-param; one doc correction landed there: §The listen dial
|
||||||
now states the port appending, the code's `host:port` form), the
|
now states the port appending, the code's `host:port` form), the
|
||||||
request-level table, and the derivation contract rules 1–6 in
|
request-level table, and the derivation contract rules 1–6 in
|
||||||
`derive_exit` (incl. `i32::MIN` via `checked_abs` → 255,
|
`derive_exit` (incl. `i32::MIN` via `checked_abs` → 255,
|
||||||
`core_dumped` never, rule 3's table entry unreachable by
|
`core_dumped` never, rule 3's table entry unreachable by
|
||||||
construction, the PTY-path rule-5 limit documented). The decline
|
construction, the PTY-path rule-5 limit documented). The decline
|
||||||
matrix is green per feature build through the harness (per-build
|
matrix is green per feature build through the harness (per-build
|
||||||
cfg-pinned modules; unset-field rows — feature on, handle unset —
|
cfg-pinned modules; the unset-field rows (feature on, handle unset)
|
||||||
present in every featured build).
|
present in every featured build).
|
||||||
- **ADR-006** ✓ — route-1 bridge (accept only after the fabric open;
|
- **ADR-006** ✓, route-1 bridge (accept only after the fabric open;
|
||||||
`ConnectFailed` on failure; the wave-2 stub retired into the real
|
`ConnectFailed` on failure; the wave-2 stub retired into the real
|
||||||
decline), exec-only git (no subsystem dispatch anywhere in the
|
decline), exec-only git (no subsystem dispatch anywhere in the
|
||||||
grammar; git service names decline), parse-only dispatch (no
|
grammar; git service names decline), parse-only dispatch (no
|
||||||
detect residue), spawn-and-return re-pinned over the real serving
|
detect residue), spawn-and-return re-pinned over the real serving
|
||||||
paths (the wave-2 mechanics rows still green; the inline listen
|
paths (the wave-2 mechanics rows still green; the inline listen
|
||||||
dial is the one recorded exception, bounded by
|
dial is the one recorded exception, bounded by
|
||||||
`LISTEN_DIAL_TIMEOUT` — wire-necessity reasoning documented).
|
`LISTEN_DIAL_TIMEOUT` with the wire-necessity reasoning documented).
|
||||||
- **ADR-003** ✓ — per-build matrices; honest advertisement
|
- **ADR-003** ✓, per-build matrices; honest advertisement
|
||||||
unchanged; the sftp feature still gates nothing and its rows
|
unchanged; the sftp feature still gates nothing and its rows
|
||||||
decline in every build (OQ-13); x11/auth-agent never-features
|
decline in every build (OQ-13); x11/auth-agent never-features
|
||||||
untouched; feature-to-dep isolation (`cargo tree` proven in
|
untouched; feature-to-dep isolation (`cargo tree` proven in
|
||||||
`w3-seam-config`).
|
`w3-seam-config`).
|
||||||
- **Auth/identity** ✓ — the resolved identity observed at every seam:
|
- **Auth/identity** ✓, the resolved identity observed at every seam:
|
||||||
alktty (the ownership witness row), alkgit (the authorize/ deny
|
alktty (the ownership witness row), alkgit (the authorize/ deny
|
||||||
row), tunnels (the CF-005/CF-006 establisher + ACL rows);
|
row), tunnels (the CF-005/CF-006 establisher + ACL rows);
|
||||||
`set_identity`/`take_call_connection` appears **nowhere in src/**
|
`set_identity`/`take_call_connection` appears **nowhere in src/**
|
||||||
(the detach decision recorded, the upstream ask booked — see below
|
(the detach decision recorded, the upstream ask booked (see below
|
||||||
for final disposition); the wave-2 `auth_succeeded` parked item
|
for final disposition); the wave-2 `auth_succeeded` parked item
|
||||||
discharged as ride-along store; no username anywhere (the
|
discharged as ride-along store; no username anywhere (the
|
||||||
safety-scan row).
|
safety-scan row).
|
||||||
- **OQ resolutions in code** ✓ — OQ-08 (unset-declines, both field
|
- **OQ resolutions in code** ✓, OQ-08 (unset-declines, both field
|
||||||
classes, `wrapper ++ [cmd]` verbatim-append), OQ-09 (env declined
|
classes, `wrapper ++ [cmd]` verbatim-append), OQ-09 (env declined
|
||||||
unconditionally), OQ-10 (derivation rules + PTY-path limit
|
unconditionally), OQ-10 (derivation rules + PTY-path limit
|
||||||
documented), OQ-11 (runtime-Unix gates compile-everywhere with
|
documented), OQ-11 (runtime-Unix gates compile-everywhere with
|
||||||
dial-call-witness-at-zero off-Unix rows), OQ-13 (sftp rows
|
dial-call-witness-at-zero off-Unix rows), OQ-13 (sftp rows
|
||||||
unchanged in every build).
|
unchanged in every build).
|
||||||
- **Bounded resources** ✓ — the cap across the fabric window (admit
|
- **Bounded resources** ✓, with the cap across the fabric window (admit
|
||||||
at the hook, release-before-reject on fabric failure, disjoint
|
at the hook, release-before-reject on fabric failure, disjoint
|
||||||
saturating release sites verified per row), the listener
|
saturating release sites verified per row), the listener
|
||||||
bookkeeping bounded by a dedicated `max_concurrent_channels`-ceiling
|
bookkeeping bounded by a dedicated `max_concurrent_channels`-ceiling
|
||||||
counter, the route map bounded by construction (minted only at
|
counter, the route map bounded by construction (minted only at
|
||||||
accepted listens, removed at cancels + session teardown — the
|
accepted listens, removed at cancels + session teardown, and the
|
||||||
`Drop` really removes from the shared router), the control-row
|
`Drop` really removes from the shared router), the control-row
|
||||||
surfaces bounded (`try_send`, never awaited on the packet loop),
|
surfaces bounded (`try_send`, never awaited on the packet loop),
|
||||||
the bridge adapters bounded (mpsc 64, 64 KiB duplexes, 16 KiB read
|
the bridge adapters bounded (mpsc 64, 64 KiB duplexes, 16 KiB read
|
||||||
buffers, `MAX_CHUNK_LEN` frame gates). No unbounded shape found in
|
buffers, `MAX_CHUNK_LEN` frame gates). No unbounded shape found in
|
||||||
any serving path.
|
any serving path.
|
||||||
- **Tracing** ✓ — the uniform event vocabulary at every
|
- **Tracing** ✓, the uniform event vocabulary at every
|
||||||
decline/serve site (row + feature_build + channel_id + class +
|
decline/serve site (row + feature_build + channel_id + class +
|
||||||
reason + exec_route; declines `debug`, the one `warn` the
|
reason + exec_route; declines `debug`, the one `warn` the
|
||||||
serving-failure arm), safety posture pinned by scan (no username,
|
serving-failure arm), safety posture pinned by scan (no username,
|
||||||
no fingerprint, no secret ever in an event).
|
no fingerprint, no secret ever in an event).
|
||||||
- **The smokes** ✓ — git clone 2.43 / `ssh -L` + curl / `ssh -tt`
|
- **The smokes** ✓, git clone 2.43 / `ssh -L` + curl / `ssh -tt`
|
||||||
pty / `ssh -R` + curl: all four landed and green in this
|
pty / `ssh -R` + curl: all four landed and green in this
|
||||||
environment; every one carries a check-gated skip-with-reason
|
environment; every one carries a check-gated skip-with-reason
|
||||||
posture, nothing silently green.
|
posture, nothing silently green.
|
||||||
- **Wave-2 booked items** ✓ — decline tracing landed
|
- **Wave-2 booked items** ✓, with decline tracing landed
|
||||||
(`w3-decline-tracing`); the acceptor vocabulary landed
|
(`w3-decline-tracing`); the acceptor vocabulary landed
|
||||||
(`w3-tunnel-listen`: harness accept hook + per-arrival cap); the
|
(`w3-tunnel-listen`: harness accept hook + per-arrival cap); the
|
||||||
deferred identity-landing assertion discharged (each seam rides the
|
deferred identity-landing assertion discharged (each seam rides the
|
||||||
resolved identity, wire-witnessed); the inherited
|
resolved identity, wire-witnessed); the inherited
|
||||||
`auth-agent`/`forwarded-streamlocal`-open watch item unchanged (no
|
`auth-agent`/`forwarded-streamlocal`-open watch item unchanged (no
|
||||||
pin touch this wave).
|
pin touch this wave).
|
||||||
- **Docs honesty** ✓ — each replaced stub's module docs name what it
|
- **Docs honesty** ✓, as each replaced stub's module docs name what it
|
||||||
now is and what remains wave-4/5; the `set_identity` reading, the
|
now is and what remains wave-4/5; the `set_identity` reading, the
|
||||||
resource-naming convention, the inline-dial bound, and
|
resource-naming convention, the inline-dial bound, and
|
||||||
portable-pty's PTY-path exit shape are all recorded in module docs
|
portable-pty's PTY-path exit shape are all recorded in module docs
|
||||||
(one overbroad Drop doc corrected inline, below).
|
(one overbroad Drop doc corrected inline, below).
|
||||||
|
|
||||||
### Interpretations (flagged at decomposition) — all confirmed
|
### Interpretations (flagged at decomposition), all confirmed
|
||||||
|
|
||||||
1. **Whole-`Option` struct per seam** (vs flat `Option` fields) —
|
1. **Whole-`Option` struct per seam** (vs flat `Option` fields),
|
||||||
confirmed: each seam is consumed atomically by exactly one seam
|
confirmed: each seam is consumed atomically by exactly one seam
|
||||||
call; the flat single-field tunnel handle stays flat. Recorded in
|
call; the flat single-field tunnel handle stays flat. Recorded in
|
||||||
`w3-seam-config`.
|
`w3-seam-config`.
|
||||||
2. **The server-side sftp bullet already discharged in wave 2** —
|
2. **The server-side sftp bullet already discharged in wave 2**, confirmed: the rows are in `tests/dispatch_core.rs` per build.
|
||||||
confirmed: the rows are in `tests/dispatch_core.rs` per build.
|
3. **The `-R` listen flow's biggest open closed without a fork**,
|
||||||
3. **The `-R` listen flow's biggest open closed without a fork** —
|
|
||||||
confirmed: the topology closed over the published alkcall 0.10.1 /
|
confirmed: the topology closed over the published alkcall 0.10.1 /
|
||||||
alktunnels 0.4.1 surfaces via the `TunnelListenSeam`
|
alktunnels 0.4.1 surfaces via the `TunnelListenSeam`
|
||||||
course-correction; a scratch probe (published crates only) proved
|
course-correction; a scratch probe (published crates only) proved
|
||||||
the flow before landing; nothing forked or absorbed.
|
the flow before landing; nothing forked or absorbed.
|
||||||
4. **The wave-2 booked items' wave-3 homes** — confirmed as above.
|
4. **The wave-2 booked items' wave-3 homes**, confirmed as above.
|
||||||
|
|
||||||
### Minor issues found and fixed inline (this session)
|
### Minor issues found and fixed inline (this session)
|
||||||
|
|
||||||
@@ -190,47 +189,47 @@ Source/doc fixes (all commit-tested; suite re-run green after):
|
|||||||
construction** (the gate's one real fix): alkcall's
|
construction** (the gate's one real fix): alkcall's
|
||||||
`OperationRegistry::register` silently *replaces* an existing op
|
`OperationRegistry::register` silently *replaces* an existing op
|
||||||
entry, contradicting the `install_forwarded_surface` docs' recorded
|
entry, contradicting the `install_forwarded_surface` docs' recorded
|
||||||
fail-closed claim — a second adapter over the same shared seam
|
fail-closed claim: a second adapter over the same shared seam
|
||||||
registry would silently steal the first adapter's arrival routes.
|
registry would silently steal the first adapter's arrival routes.
|
||||||
`install_forwarded_surface` now pre-checks the op
|
`install_forwarded_surface` now pre-checks the op
|
||||||
(`registry.registration(OP_TUNNEL_FORWARDED)`) and refuses with
|
(`registry.registration(OP_TUNNEL_FORWARDED)`) and refuses with
|
||||||
`AdapterError::ListenRegistration`; new wire-level row
|
`AdapterError::ListenRegistration`; new wire-level row
|
||||||
`a_second_adapter_over_a_shared_listen_registry_fails_construction`
|
`a_second_adapter_over_a_shared_listen_registry_fails_construction`
|
||||||
pins both constructs/refuse.
|
pins both constructs/refuse.
|
||||||
- **channel_cap.rs** — the bridge-failure release doc said "after the
|
- **channel_cap.rs**, the bridge-failure release doc said "after the
|
||||||
reject is answered"; the code (and the pinned test) release
|
reject is answered"; the code (and the pinned test) release
|
||||||
*before* the reject. Doc corrected.
|
*before* the reject. Doc corrected.
|
||||||
- **handler.rs** — the `Drop` doc claimed "every spawned serving task
|
- **handler.rs**, the `Drop` doc claimed "every spawned serving task
|
||||||
watches" the teardown signal; scoping corrected (the session rows
|
watches" the teardown signal; scoping corrected (the session rows
|
||||||
watch; the git task ends through the duplex, bounded by alkgit's
|
watch; the git task ends through the duplex, bounded by alkgit's
|
||||||
`session_wall_clock`). The cancel-form doc's recorded ordering
|
`session_wall_clock`). The cancel-form doc's recorded ordering
|
||||||
(router-before-map) was inverted vs the code; corrected, with the
|
(router-before-map) was inverted vs the code; corrected, with the
|
||||||
post-cancel-race caveat recorded.
|
post-cancel-race caveat recorded.
|
||||||
- **tunnel_map.rs** — the "never half-serves" bookkeeping claim now
|
- **tunnel_map.rs**, the "never half-serves" bookkeeping claim now
|
||||||
records the one-arrival caveat: an arrival that cloned its route
|
records the one-arrival caveat: an arrival that cloned its route
|
||||||
just before a cancel completes independently (never half-open;
|
just before a cancel completes independently (never half-open;
|
||||||
sshd parity).
|
sshd parity).
|
||||||
- **tty_map.rs** — stale ADR cross-ref on the exit chunk (alktty
|
- **tty_map.rs**, a stale ADR cross-ref on the exit chunk (alktty
|
||||||
ADR-005 → ADR-004 on the alknet ADR-055 lineage).
|
ADR-005 → ADR-004 on the alknet ADR-055 lineage).
|
||||||
- **lib.rs** — the crate-level wave-state doc still said "wave 2
|
- **lib.rs**, the crate-level wave-state doc still said "wave 2
|
||||||
decomposes after this gate"; updated to the landed state.
|
decomposes after this gate"; updated to the landed state.
|
||||||
- **tests/dispatch_core.rs** — the stale
|
- **tests/dispatch_core.rs**, the stale
|
||||||
`unknown_git_services_route_to_the_pipe_row` name/doc superseded by
|
`unknown_git_services_route_to_the_pipe_row` name/doc superseded by
|
||||||
the w3 decision ("off the pipe row"); renamed, verdicts preserved
|
the w3 decision ("off the pipe row"); renamed, verdicts preserved
|
||||||
(the wire shape coincides — why it never failed).
|
(the wire shape coincides, which is why it never failed).
|
||||||
- **handler.rs (git row)** — a pty-then-git-exec in a combined build
|
- **handler.rs (git row)**, a pty-then-git-exec in a combined build
|
||||||
left the stored pty entry until close; the git take now drops it
|
left the stored pty entry until close; the git take now drops it
|
||||||
(the pipe row's consumption shape).
|
(the pipe row's consumption shape).
|
||||||
- **mapping.md** — the §listen-dial resource wording corrected to the
|
- **mapping.md**, the §listen-dial resource wording corrected to the
|
||||||
code's assembled form (address verbatim + tcp port appended;
|
code's assembled form (address verbatim + tcp port appended;
|
||||||
`0` stays `0`).
|
`0` stays `0`).
|
||||||
|
|
||||||
### Booked findings (not remediation this gate — wave-4 material)
|
### Booked findings (not remediation this gate, wave-4 material)
|
||||||
|
|
||||||
1. **The git row's status-before-close is honored by scheduling, not
|
1. **The git row's status-before-close is honored by scheduling, not
|
||||||
by construction** (the gate's only structural moderate):
|
by construction** (the gate's only structural moderate):
|
||||||
`serve_duplex_resolved` splits the channel stream internally and
|
`serve_duplex_resolved` splits the channel stream internally and
|
||||||
its session halves drop when the serve returns — russh's
|
its session halves drop when the serve returns, so russh's
|
||||||
`ChannelCloseOnDrop` rx-half emits a server→client
|
`ChannelCloseOnDrop` rx-half emits a server→client
|
||||||
`CHANNEL_CLOSE` *inside* the seam call, before
|
`CHANNEL_CLOSE` *inside* the seam call, before
|
||||||
`serve_git_row`'s explicit `exit_status_request`. Today the
|
`serve_git_row`'s explicit `exit_status_request`. Today the
|
||||||
@@ -240,11 +239,11 @@ Source/doc fixes (all commit-tested; suite re-run green after):
|
|||||||
implicit close first and silently drop the report (russh no-ops
|
implicit close first and silently drop the report (russh no-ops
|
||||||
`exit_status_request` for an already-closed channel). A fix wants
|
`exit_status_request` for an already-closed channel). A fix wants
|
||||||
a seam/shape change (split halves into the seam, or queue the
|
a seam/shape change (split halves into the seam, or queue the
|
||||||
report while the stream is alive) — beyond this gate's inline
|
report while the stream is alive), beyond this gate's inline
|
||||||
budget; book a wave-4 hardening item. (The tty rows are NOT in
|
budget; book a wave-4 hardening item. (The tty rows are NOT in
|
||||||
this class: their report precedes any close-contracting drop in
|
this class: their report precedes any close-contracting drop in
|
||||||
one task.)
|
one task.)
|
||||||
2. **The `channel-not-held` guard lacks wire test rows** — a
|
2. **The `channel-not-held` guard lacks wire test rows**, a
|
||||||
documented decline class on all three rows (git/tty pipe/shell),
|
documented decline class on all three rows (git/tty pipe/shell),
|
||||||
constructible on the wire (a second exec on a served channel),
|
constructible on the wire (a second exec on a served channel),
|
||||||
but no row asserts it in any build; mapping.md's every-row
|
but no row asserts it in any build; mapping.md's every-row
|
||||||
@@ -255,14 +254,14 @@ Source/doc fixes (all commit-tested; suite re-run green after):
|
|||||||
the wire (only the teardown signal is); the Closed/Break
|
the wire (only the teardown signal is); the Closed/Break
|
||||||
no-exit-chunk close shape has no wire row; the tty stdout
|
no-exit-chunk close shape has no wire row; the tty stdout
|
||||||
`write_all` carries no deadline (backpressure bounded, liveness
|
`write_all` carries no deadline (backpressure bounded, liveness
|
||||||
covered by inactivity timeout — record as a known limit only).
|
covered by inactivity timeout; record it as a known limit only).
|
||||||
4. **Defensive `ListenRouter` token-collision pre-check** (minor):
|
4. **Defensive `ListenRouter` token-collision pre-check** (minor):
|
||||||
`register` silently replaces on token collision; safety rests on
|
`register` silently replaces on token collision; safety rests on
|
||||||
the fabric's ADR-008 mint discipline. A fail-closed
|
the fabric's ADR-008 mint discipline. A fail-closed
|
||||||
decline-the-listen-on-collision is a cheap wave-4 addition.
|
decline-the-listen-on-collision is a cheap wave-4 addition.
|
||||||
5. **Upstream asks to file** (booked with their owners, not this
|
5. **Upstream asks to file** (booked with their owners, not this
|
||||||
crate): the alktunnels/alkcall "non-detaching post-construction
|
crate): the alktunnels/alkcall "non-detaching post-construction
|
||||||
identity seam" note (`w3-tunnel-direct`'s recorded decision — the
|
identity seam" note (`w3-tunnel-direct`'s recorded decision, under which the
|
||||||
door keeps the no-detach posture until upstream proposes a seam),
|
door keeps the no-detach posture until upstream proposes a seam),
|
||||||
and mapping.md's recorded event-vocabulary nuance for the
|
and mapping.md's recorded event-vocabulary nuance for the
|
||||||
unreachable mismatch arms (`correlation-miss` naming).
|
unreachable mismatch arms (`correlation-miss` naming).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: review-wave-4
|
id: review-wave-4
|
||||||
name: Wave 4 gate — the client-direction review
|
name: "Wave 4 gate: the client-direction review"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w4-dial, w4-op-rows, w4-reverse-listen, w4-lifecycle, w4-git-exit-close, w4-channel-not-held-rows, w4-tty-coverage, w4-router-collision]
|
depends_on: [w4-dial, w4-op-rows, w4-reverse-listen, w4-lifecycle, w4-git-exit-close, w4-channel-not-held-rows, w4-tty-coverage, w4-router-collision]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -12,49 +12,49 @@ level: review
|
|||||||
## Description
|
## Description
|
||||||
|
|
||||||
The wave-4 review gate. The client direction completes the
|
The wave-4 review gate. The client direction completes the
|
||||||
two-direction door — wave 5 builds the full matrix and the release on
|
two-direction door, and wave 5 builds the full matrix and the release on
|
||||||
both directions, and the published crate freezes this surface. The
|
both directions, and the published crate freezes this surface. The
|
||||||
gate reads the landed client direction against the architecture line
|
gate reads the landed client direction against the architecture line
|
||||||
by line:
|
by line:
|
||||||
|
|
||||||
- **client.md line-by-line** — §The dial surface (key source
|
- **client.md line-by-line**, reading it section by section: §The dial surface (key source
|
||||||
decrypt-at-dial fail-closed; the policy at `check_server_key`; the
|
decrypt-at-dial fail-closed; the policy at `check_server_key`; the
|
||||||
bounded knobs with the keepalive 15 s / 3 posture), §Op rows (each
|
bounded knobs with the keepalive 15 s / 3 posture), §Op rows (each
|
||||||
table row read against the landed code — the POC-3 §D.3 mechanics
|
table row read against the landed code, the POC-3 §D.3 mechanics
|
||||||
pinned in code + rows), §The acceptor (served only under a
|
pinned in code + rows), §The acceptor (served only under a
|
||||||
registered feature's acceptor; unregistered/unfeatured declined
|
registered feature's acceptor; unregistered/unfeatured declined
|
||||||
cleanly — never accept-and-drop; late-install across a live
|
cleanly, never accept-and-drop; late-install across a live
|
||||||
connection), §Lifecycle primitives (explicitly cancellable handles;
|
connection), §Lifecycle primitives (explicitly cancellable handles;
|
||||||
`Disconnect::ByApplication`; **no reconnect/restore policy in the
|
`Disconnect::ByApplication`; **no reconnect/restore policy in the
|
||||||
crate** — the boundary held).
|
crate**, a boundary held).
|
||||||
- **ADR-003 symmetry** — the same features gate both directions; the
|
- **ADR-003 symmetry**, where the same features gate both directions; the
|
||||||
per-build matrices green client-side (the acceptor rows per build);
|
per-build matrices green client-side (the acceptor rows per build);
|
||||||
honest advertisement unchanged; the sftp placeholder declines in
|
honest advertisement unchanged; the sftp placeholder declines in
|
||||||
every build both directions (OQ-13); x11/auth-agent never-features
|
every build both directions (OQ-13); x11/auth-agent never-features
|
||||||
untouched.
|
untouched.
|
||||||
- **ADR-005 / OQ-05 / OQ-07** — decrypt-at-dial with every
|
- **ADR-005 / OQ-05 / OQ-07**, decrypt-at-dial with every
|
||||||
`KeySourceError` posture a no-dial outcome; the policy seam
|
`KeySourceError` posture a no-dial outcome; the policy seam
|
||||||
consumed at the hook (crate default connects to nothing; no
|
consumed at the hook (crate default connects to nothing; no
|
||||||
accept-all built-in); the `Zeroizing` wrap posture intact.
|
accept-all built-in); the `Zeroizing` wrap posture intact.
|
||||||
- **The wave-3 gate's booked items 1–4** — every one discharged or
|
- **The wave-3 gate's booked items 1–4**, every one discharged or
|
||||||
explicitly re-booked: the git status-before-close by construction
|
explicitly re-booked: the git status-before-close by construction
|
||||||
(`w4-git-exit-close`), the channel-not-held wire rows
|
(`w4-git-exit-close`), the channel-not-held wire rows
|
||||||
(`w4-channel-not-held-rows`), the tty coverage minors
|
(`w4-channel-not-held-rows`), the tty coverage minors
|
||||||
(`w4-tty-coverage`), the router token-collision pre-check
|
(`w4-tty-coverage`), the router token-collision pre-check
|
||||||
(`w4-router-collision`). Item 5 (the upstream asks) verified still
|
(`w4-router-collision`). Item 5 (the upstream asks) verified still
|
||||||
booked with their owners — not absorbed silently.
|
booked with their owners, not absorbed silently.
|
||||||
- **Bounded resources** — no new unbounded shape on the client
|
- **Bounded resources**, with no new unbounded shape on the client
|
||||||
surfaces: the acceptor registry bounded, the listener bookkeeping
|
surfaces: the acceptor registry bounded, the listener bookkeeping
|
||||||
bounded, the pumps bounded (the convention-9 read over every new
|
bounded, the pumps bounded (the convention-9 read over every new
|
||||||
serving/pumping path).
|
serving/pumping path).
|
||||||
- **Tracing** — the uniform event vocabulary across the new
|
- **Tracing**, the uniform event vocabulary across the new
|
||||||
client-side decline/serve paths; the safety posture held (no
|
client-side decline/serve paths; the safety posture held (no
|
||||||
secrets, no key material, no username).
|
secrets, no key material, no username).
|
||||||
- **The smokes** — the client-direction real-tool postures recorded
|
- **The smokes**, with the client-direction real-tool postures recorded
|
||||||
honestly (required/conditionally-skipped with reason); nothing
|
honestly (required/conditionally-skipped with reason); nothing
|
||||||
silently green; anything named wave-5 e2e territory named
|
silently green; anything named wave-5 e2e territory named
|
||||||
explicitly.
|
explicitly.
|
||||||
- **Docs honesty** — module docs name what each new surface is, what
|
- **Docs honesty**: module docs name what each new surface is, what
|
||||||
remains wave-5 material, and any new upstream notes (the russh
|
remains wave-5 material, and any new upstream notes (the russh
|
||||||
client-hook shapes at this pin; the acceptor-registry shape
|
client-hook shapes at this pin; the acceptor-registry shape
|
||||||
decision; the DialConfig disposition).
|
decision; the DialConfig disposition).
|
||||||
@@ -96,75 +96,75 @@ match `docs/plans/implementation.md`'s decomposition record; every
|
|||||||
task file carries its completion records and checked acceptance
|
task file carries its completion records and checked acceptance
|
||||||
criteria.
|
criteria.
|
||||||
|
|
||||||
- **client.md line-by-line** ✓ — read against the landed code:
|
- **client.md line-by-line** ✓, read against the landed code:
|
||||||
§The dial surface (`client.rs` — the four-step fail-closed
|
§The dial surface (`client.rs`, the four-step fail-closed
|
||||||
sequence: decrypt-at-dial key load, the bounded russh `Config`
|
sequence: decrypt-at-dial key load, the bounded russh `Config`
|
||||||
with the keepalive 15 s / 3 posture and the always-set wall clock,
|
with the keepalive 15 s / 3 posture and the always-set wall clock,
|
||||||
the policy at `check_server_key`, the single publickey-only auth;
|
the policy at `check_server_key`, the single publickey-only auth;
|
||||||
the sanctioned `username` `DialConfig` amendment recorded honestly
|
the sanctioned `username` `DialConfig` amendment recorded honestly
|
||||||
— a required parameter, no crate default, never identity, never
|
(a required parameter, no crate default, never identity, never
|
||||||
logged); §Op rows (`client_ops.rs` — every table row present: the
|
logged); §Op rows, covering `client_ops.rs`, where every table row is present: the
|
||||||
exec/pty/shell rows under `session`, the direct rows under
|
exec/pty/shell rows under `session`, the direct rows under
|
||||||
`tunnel`, the git exec row under `git` over the shared grammar's
|
`tunnel`, the git exec row under `git` over the shared grammar's
|
||||||
construction arm, the sftp placeholder in every build; POC-3
|
construction arm, the sftp placeholder in every build; POC-3
|
||||||
§D.3 mechanics pinned in code + rows — `exec()` not
|
§D.3 mechanics pinned in code plus rows (`exec()` not `request_exec`,
|
||||||
`request_exec`, the `wait()`-drain Success/Failure, split-halves
|
`request_exec`, the `wait()`-drain Success/Failure, split-halves
|
||||||
wait-pumps for the exit-consuming rows and straight
|
wait-pumps for the exit-consuming rows and straight
|
||||||
`into_stream` for the exit-less, the fire-and-forget controls
|
`into_stream` for the exit-less, the fire-and-forget controls
|
||||||
sent from the live pump, no queue consumption before any
|
sent from the live pump, no queue consumption before any
|
||||||
conversion); §The acceptor (`client_accept.rs` + the
|
conversion); §The acceptor (`client_accept.rs` + the
|
||||||
`client_handler.rs` hooks — per-type fixed slots, refuse-never-
|
`client_handler.rs` hooks, being per-type fixed slots, refuse-never-
|
||||||
replace, arrival-time reads, decline-not-buffer, late-install
|
replace, arrival-time reads, decline-not-buffer, late-install
|
||||||
over the live connection, listen rows gated pre-wire, the two
|
over the live connection, listen rows gated pre-wire, the two
|
||||||
dedicated counters against the one ceiling); §Lifecycle
|
dedicated counters against the one ceiling); §Lifecycle
|
||||||
primitives (`client_accept.rs` handles + `client.rs` §Lifecycle
|
primitives (`client_accept.rs` handles + `client.rs` §Lifecycle
|
||||||
posture — explicit cancels, `Disconnect::ByApplication` the only
|
posture, with explicit cancels, `Disconnect::ByApplication` the only
|
||||||
clean path, drop-is-never-the-clean-path row-pinned both ways,
|
clean path, drop-is-never-the-clean-path row-pinned both ways,
|
||||||
the no-reconnect/restore-policy boundary stated in module docs;
|
the no-reconnect/restore-policy boundary stated in module docs;
|
||||||
the crate owns no policy). client.md's landing blocks (acceptor/
|
the crate owns no policy). client.md's landing blocks (acceptor/
|
||||||
lifecycle/op-row shapes recorded at landing) are in the doc.
|
lifecycle/op-row shapes recorded at landing) are in the doc.
|
||||||
- **ADR-003 symmetry** ✓ — the features gate both directions
|
- **ADR-003 symmetry** ✓, as the features gate both directions
|
||||||
(`Cargo.toml` unchanged: `session`/`git`/`tunnel`/`sftp`,
|
(`Cargo.toml` unchanged: `session`/`git`/`tunnel`/`sftp`,
|
||||||
`default = []`); the client exports ride the same gates
|
`default = []`); the client exports ride the same gates
|
||||||
(`RemoteSession` under `session`, `TunnelChannel` + the acceptor
|
(`RemoteSession` under `session`, `TunnelChannel` + the acceptor
|
||||||
surface + `ReverseListener` under `tunnel`, `GitPackSession`
|
surface + `ReverseListener` under `tunnel`, `GitPackSession`
|
||||||
under `git`, the sftp row feature-free); the per-build matrices
|
under `git`, the sftp row feature-free); the per-build matrices
|
||||||
are green (the default build's 16 binaries + per-feature/
|
are green (the default build's 16 binaries plus per-feature/
|
||||||
`--all-features`; the unfeatured builds' fail-closed wire rows
|
`--all-features`; the unfeatured builds' fail-closed wire rows
|
||||||
ride `tests/client_dial.rs`'s decline-all row — the
|
ride `tests/client_dial.rs`'s decline-all row, and the
|
||||||
`client_handler.rs` hooks are cfg-declined in every build);
|
`client_handler.rs` hooks are cfg-declined in every build);
|
||||||
honest advertisement unchanged; x11/agent never-features
|
honest advertisement unchanged; x11/agent never-features
|
||||||
untouched on both directions.
|
untouched on both directions.
|
||||||
- **ADR-005 / OQ-05 / OQ-07** ✓ — decrypt-at-dial with every
|
- **ADR-005 / OQ-05 / OQ-07** ✓, decrypt-at-dial with every
|
||||||
`KeySourceError` posture a no-dial outcome (`DialError::KeySource`
|
`KeySourceError` posture a no-dial outcome (`DialError::KeySource`
|
||||||
preserved per-variant; `error.rs` taxonomy rows); the policy seam
|
preserved per-variant; `error.rs` taxonomy rows); the policy seam
|
||||||
consumed at the hook (`client_handler.rs::check_server_key` — the
|
consumed at the hook (`client_handler.rs::check_server_key`, where the
|
||||||
fingerprint rides the crate's single path, `PresentedHostKey`
|
fingerprint rides the crate's single path, `PresentedHostKey`
|
||||||
carries host/port, the verdict passes through; `RejectAll`
|
carries host/port, the verdict passes through; `RejectAll`
|
||||||
connects to nothing, no accept-all built-in); the wave-1
|
connects to nothing, no accept-all built-in); the wave-1
|
||||||
`Zeroizing` wrap posture intact (`key_source.rs`, untouched).
|
`Zeroizing` wrap posture intact (`key_source.rs`, untouched).
|
||||||
- **The wave-3 gate's booked items 1–4** ✓ — all discharged:
|
- **The wave-3 gate's booked items 1–4** ✓, all discharged:
|
||||||
item 1 (`w4-git-exit-close`) — the split-halves seam duplex
|
item 1 (`w4-git-exit-close`), the split-halves seam duplex
|
||||||
(`ChannelHalveReader` over the owned close-free read half; no
|
(`ChannelHalveReader` over the owned close-free read half; no
|
||||||
drop path ever messages the wire) with the by-construction
|
drop path ever messages the wire) with the by-construction
|
||||||
property wire-pinned and the row falsifying the old shape
|
property wire-pinned and the row falsifying the old shape
|
||||||
deterministic-red-under-revert; item 2
|
deterministic-red-under-revert; item 2
|
||||||
(`w4-channel-not-held-rows`) — the guard's wire rows on all
|
(`w4-channel-not-held-rows`), the guard's wire rows on all
|
||||||
three served rows per build, vocabulary-asserting; item 3
|
three served rows per build, vocabulary-asserting; item 3
|
||||||
(`w4-tty-coverage`) — the `channel_close`-fired cancel path
|
(`w4-tty-coverage`), the `channel_close`-fired cancel path
|
||||||
wire-witnessed, the Closed/Break no-exit-chunk shape row-pinned
|
wire-witnessed, the Closed/Break no-exit-chunk shape row-pinned
|
||||||
(Break's frame classification unit-rowed, wire-unreachable,
|
(Break's frame classification unit-rowed, wire-unreachable,
|
||||||
recorded), the `write_all` no-deadline limit recorded in module
|
recorded), the `write_all` no-deadline limit recorded in module
|
||||||
docs; item 4 (`w4-router-collision`) — the register-refuses
|
docs; item 4 (`w4-router-collision`), the register-refuses
|
||||||
pre-check with the error-surface decision recorded honestly (the
|
pre-check with the error-surface decision recorded honestly (the
|
||||||
wire-time decline disposition, not a taxonomy variant that would
|
wire-time decline disposition, not a taxonomy variant that would
|
||||||
kill the session). **Item 5 verified still booked with its
|
kill the session). **Item 5 verified still booked with its
|
||||||
owners, not absorbed**: no `set_identity`/`take_call_connection`
|
owners and not absorbed**: no `set_identity`/`take_call_connection`
|
||||||
anywhere in `src/` code (doc references only, recording the
|
anywhere in `src/` code (doc references only, recording the
|
||||||
no-detach posture + the upstream ask), and mapping.md's
|
no-detach posture + the upstream ask), and mapping.md's
|
||||||
correlation-miss event-vocabulary nuance records stand.
|
correlation-miss event-vocabulary nuance records stand.
|
||||||
- **Bounded resources** ✓ (convention 9 read over every new
|
- **Bounded resources** ✓, with the convention-9 read over every new
|
||||||
client serving/pumping path) — the acceptor registry bounded by
|
client serving/pumping path showing the acceptor registry bounded by
|
||||||
construction (two slots); the reverse-listen count and the
|
construction (two slots); the reverse-listen count and the
|
||||||
accepted-open handoff count are two dedicated `ChannelCap`
|
accepted-open handoff count are two dedicated `ChannelCap`
|
||||||
counters against the same `Limits::max_concurrent_channels`
|
counters against the same `Limits::max_concurrent_channels`
|
||||||
@@ -174,37 +174,37 @@ criteria.
|
|||||||
launch-drain cap, russh windows verbatim, the always-on wall
|
launch-drain cap, russh windows verbatim, the always-on wall
|
||||||
clock); the git row's command/repo gates answer pre-wire. One
|
clock); the git row's command/repo gates answer pre-wire. One
|
||||||
gap found and fixed inline (below).
|
gap found and fixed inline (below).
|
||||||
- **Tracing** ✓ — the client-side decline paths ride the uniform
|
- **Tracing** ✓, with the client-side decline paths riding the uniform
|
||||||
vocabulary (`decline_open` with `no-acceptor` / `cap-exceeded` /
|
vocabulary (`decline_open` with `no-acceptor` / `cap-exceeded` /
|
||||||
`never-feature`; the skeleton's decline rows event-pinned); the
|
`never-feature`; the skeleton's decline rows event-pinned); the
|
||||||
safety posture held — structurally no username/fingerprint/secret
|
safety posture held, as structurally no username/fingerprint/secret
|
||||||
parameter exists on any event helper (the `&'static str`
|
parameter exists on any event helper (the `&'static str`
|
||||||
vocabulary); the client op rows' typed surfaces carry outcomes
|
vocabulary); the client op rows' typed surfaces also carry outcomes
|
||||||
without events (the recorded asymmetry decision).
|
without events (the recorded asymmetry decision).
|
||||||
- **The smokes** ✓ — three recorded honestly: the alkssh-client ↔
|
- **The smokes** ✓, three recorded honestly: the alkssh-client ↔
|
||||||
alkssh-server dial smoke (`tests/client_dial.rs`), the client-exec
|
alkssh-server dial smoke (`tests/client_dial.rs`), the client-exec
|
||||||
↔ server-session-rows smoke (`tests/client_ops.rs`, check-gated
|
↔ server-session-rows smoke (`tests/client_ops.rs`, check-gated
|
||||||
on `sh`), the full-circle reverse-listen smoke
|
on `sh`), the full-circle reverse-listen smoke
|
||||||
(`tests/tunnel_mapping.rs`); real-`sshd` e2e named wave-5
|
(`tests/tunnel_mapping.rs`); real-`sshd` e2e named wave-5
|
||||||
territory explicitly in each task's records — nothing silently
|
territory explicitly in each task's records, with nothing silently
|
||||||
green, nothing dropped.
|
green, nothing dropped.
|
||||||
- **Docs honesty** ✓ — module docs name each new surface and its
|
- **Docs honesty** ✓, as module docs name each new surface and its
|
||||||
remaining wave-5 material (the fake-`sshd` posture, the
|
remaining wave-5 material (the fake-`sshd` posture, the
|
||||||
DialConfig amendment, the russh client hook shapes at this pin,
|
DialConfig amendment, the russh client hook shapes at this pin,
|
||||||
the acceptor-registry shape decision, the recorded wire facts —
|
the acceptor-registry shape decision, and the recorded wire facts
|
||||||
the port-0-only mint, the drop/is-closed semantics); one stale
|
the port-0-only mint, the drop/is-closed semantics); one stale
|
||||||
crate-level sentence fixed inline (below).
|
crate-level sentence fixed inline (below).
|
||||||
|
|
||||||
### Minor issues found and fixed inline (this session)
|
### Minor issues found and fixed inline (this session)
|
||||||
|
|
||||||
- **The accepted-open handoff counter's panic-unsafe release** (the
|
- **The accepted-open handoff counter's panic-unsafe release**, the
|
||||||
gate's one code fix): the counter released only after
|
gate's one code fix: the counter released only after
|
||||||
`acceptor.accept(...)` returned — an assembly acceptor panicking
|
`acceptor.accept(...)` returned, so an assembly acceptor panicking
|
||||||
inside `accept` stranded one held slot for the connection's
|
inside `accept` would strand one held slot for the connection's
|
||||||
lifetime (a bounded-resource leak path on the new cap). Fixed
|
lifetime, a bounded-resource leak path on the new cap. Fixed
|
||||||
with a panic-safe guard (`HandoffCapGuard` in
|
with a panic-safe guard (`HandoffCapGuard` in
|
||||||
`src/client_accept.rs`): the release fires at the handoff task's
|
`src/client_accept.rs`): the release fires at the handoff task's
|
||||||
end, on completion *or unwind*; the recorded release-site wording
|
end, whether on completion or unwind; the recorded release-site wording
|
||||||
updated in the module docs, the trait docs, and
|
updated in the module docs, the trait docs, and
|
||||||
`client_handler.rs`'s two references; a unit row
|
`client_handler.rs`'s two references; a unit row
|
||||||
(`the_handoff_guard_releases_on_unwind`) pins the unwind release.
|
(`the_handoff_guard_releases_on_unwind`) pins the unwind release.
|
||||||
@@ -214,23 +214,23 @@ criteria.
|
|||||||
`w4-lifecycle`; the same staleness class the wave-3 gate fixed
|
`w4-lifecycle`; the same staleness class the wave-3 gate fixed
|
||||||
inline). Corrected to the landed state.
|
inline). Corrected to the landed state.
|
||||||
|
|
||||||
### Interpretations (flagged at decomposition) — all confirmed
|
### Interpretations (flagged at decomposition), all confirmed
|
||||||
|
|
||||||
1. **The fail-closed client Handler skeleton in `w4-dial`, the
|
1. **The fail-closed client Handler skeleton in `w4-dial`, the
|
||||||
registry in `w4-reverse-listen`** — confirmed: the skeleton's
|
registry in `w4-reverse-listen`**, confirmed: the skeleton's
|
||||||
decline-all default is row-pinned in every build, and the
|
decline-all default is row-pinned in every build, and the
|
||||||
registry grows exactly the two forwarded hooks; the split kept
|
registry grows exactly the two forwarded hooks; the split kept
|
||||||
both surfaces reviewable.
|
both surfaces reviewable.
|
||||||
2. **The fake-`sshd` client-direction vehicle in `tests/common`** —
|
2. **The fake-`sshd` client-direction vehicle in `tests/common`**,
|
||||||
confirmed: vanilla-parity, real loopback TCP, one dial per
|
confirmed: vanilla-parity, real loopback TCP, one dial per
|
||||||
instance, and the harness's `ForwardedAcceptor` vocabulary was
|
instance, and the harness's `ForwardedAcceptor` vocabulary was
|
||||||
grown (not forked) into `client_accept.rs`'s real surface.
|
grown (not forked) into `client_accept.rs`'s real surface.
|
||||||
3. **No `DialConfig` course-correction anticipated** — confirmed,
|
3. **No `DialConfig` course-correction anticipated** was confirmed,
|
||||||
with the one sanctioned exception recorded in-task (the required
|
with the one sanctioned exception recorded in-task (the required
|
||||||
`username` — the USERAUTH_REQUEST's structural requirement; the
|
`username`, the USERAUTH_REQUEST's structural requirement; the
|
||||||
amendment follows the `w3-seam-config` pattern and is honestly
|
amendment follows the `w3-seam-config` pattern and is honestly
|
||||||
documented).
|
documented).
|
||||||
4. **The hardening tasks ordered by single-writer needs** —
|
4. **The hardening tasks ordered by single-writer needs** were
|
||||||
confirmed: `tests/git_mapping.rs` then `tests/tty_mapping.rs`
|
confirmed: `tests/git_mapping.rs` then `tests/tty_mapping.rs`
|
||||||
sequencing held, no cross-task file conflicts in the landed
|
sequencing held, no cross-task file conflicts in the landed
|
||||||
history.
|
history.
|
||||||
@@ -259,9 +259,9 @@ and the lifecycle primitives with the drop/clean-close boundary
|
|||||||
row-pinned both ways and the no-policy posture stated. All three
|
row-pinned both ways and the no-policy posture stated. All three
|
||||||
interpretations confirmed except as recorded (all favorable); the
|
interpretations confirmed except as recorded (all favorable); the
|
||||||
wave-3 booked items 1–4 verified discharged and item 5 verified
|
wave-3 booked items 1–4 verified discharged and item 5 verified
|
||||||
still booked upstream — nothing absorbed. The full verification set
|
still booked upstream, with nothing absorbed. The full verification set
|
||||||
is green. Two minors fixed inline (the handoff counter's
|
is green. Two minors fixed inline (the handoff counter's
|
||||||
panic-unsafe release — the one code fix, guard + unit row; a stale
|
panic-unsafe release, the one code fix with its guard and unit row; a stale
|
||||||
crate-level wave-state doc sentence). No structural findings; wave
|
crate-level wave-state doc sentence). No structural findings; wave
|
||||||
5 decomposition is unblocked and should absorb nothing beyond its
|
5 decomposition is unblocked and should absorb nothing beyond its
|
||||||
own plan (the decline matrix, real-tool e2e, fuzzing, docs, the
|
own plan (the decline matrix, real-tool e2e, fuzzing, docs, the
|
||||||
@@ -269,12 +269,12 @@ publish).
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
**Verdict: green** (2026-10-29) — see Notes for the full gate
|
**Verdict: green** (2026-10-29); see Notes for the full gate
|
||||||
reading. All eight wave-4 tasks verified against the client.md/ADR
|
reading. All eight wave-4 tasks verified against the client.md/ADR
|
||||||
contract surfaces line by line; the wave-3 gate's booked items 1–4
|
contract surfaces line by line; the wave-3 gate's booked items 1–4
|
||||||
discharged, item 5 verified still booked upstream; the full
|
discharged, item 5 verified still booked upstream; the full
|
||||||
verification set + `taskgraph validate` green. Two inline fixes
|
verification set + `taskgraph validate` green. Two inline fixes
|
||||||
landed with the gate: the accepted-open handoff counter's
|
landed with the gate: the accepted-open handoff counter's
|
||||||
panic-safe release (`HandoffCapGuard` — the gate's one code fix)
|
panic-safe release (`HandoffCapGuard`, the gate's one code fix)
|
||||||
and the stale crate-level wave-state doc sentence in `lib.rs`.
|
and the stale crate-level wave-state doc sentence in `lib.rs`.
|
||||||
Wave 5 decomposition is unblocked.
|
Wave 5 decomposition is unblocked.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: review-wave-5
|
id: review-wave-5
|
||||||
name: Wave 5 gate — the release review
|
name: "Wave 5 gate: the release review"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w5-decline-matrix, w5-e2e-server, w5-e2e-client, w5-fuzz-harness, w5-release-readiness]
|
depends_on: [w5-decline-matrix, w5-e2e-server, w5-e2e-client, w5-fuzz-harness, w5-release-readiness]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -11,40 +11,40 @@ level: review
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
The wave-5 review gate — the release review that closes the plan
|
The wave-5 review gate, the release review that closes the plan
|
||||||
(`docs/plans/implementation.md` §Wave 5: "then **the publish of
|
(`docs/plans/implementation.md` §Wave 5: "then **the publish of
|
||||||
`alkssh 0.1`** — the gate review that closes the plan"). The gate
|
`alkssh 0.1`**, which is the gate review that closes the plan"). The gate
|
||||||
stays a **pure review**; the publish is its own task (`w5-publish`)
|
stays a **pure review**; the publish is its own task (`w5-publish`)
|
||||||
executing only over a green verdict. The gate reads:
|
executing only over a green verdict. The gate reads:
|
||||||
|
|
||||||
- **The decline-matrix suite** — the coverage-audit table read
|
- **The decline-matrix suite**, reading the coverage-audit table
|
||||||
against `mapping.md`'s matrix table cell by cell: every row ×
|
against `mapping.md`'s matrix table cell by cell: every row ×
|
||||||
every build it can appear in × both directions pinned; the sftp
|
every build it can appear in × both directions pinned; the sftp
|
||||||
placeholder rows and the never-features explicit in every build;
|
placeholder rows and the never-features explicit in every build;
|
||||||
the matrix runner in the verification list.
|
the matrix runner in the verification list.
|
||||||
- **The e2e postures** — both directions' real-tool rows read:
|
- **The e2e postures**, reading both directions' real-tool rows:
|
||||||
nothing silently green (every skip reasoned with the tool/version
|
nothing silently green (every skip reasoned with the tool/version
|
||||||
cause), every e2e finding discharged or explicitly booked with an
|
cause), every e2e finding discharged or explicitly booked with an
|
||||||
owner (never absorbed); the real-`sshd` client-direction rows
|
owner, never absorbed; the real-`sshd` client-direction rows
|
||||||
verified against the reverse-runner shape.
|
verified against the reverse-runner shape.
|
||||||
- **The fuzz corpus gate** — the sidecar's layout read against the
|
- **The fuzz corpus gate**, reading the sidecar's layout against the
|
||||||
alkgit convention (detached runner, committed seeds + no-op-diff
|
alkgit convention (detached runner, committed seeds + no-op-diff
|
||||||
generator, the shared-library corpus replay in the verification
|
generator, the shared-library corpus replay in the verification
|
||||||
list); the campaign logs read (no unresolved crash/OOM/timeout);
|
list); the campaign logs read (no unresolved crash/OOM/timeout);
|
||||||
the exposure shape read (no non-`doc(hidden)` surface growth).
|
the exposure shape read (no non-`doc(hidden)` surface growth).
|
||||||
- **Release readiness** — rustdoc warning-free × builds; README/
|
- **Release readiness**: rustdoc warning-free across builds; README/
|
||||||
CHANGELOG read for honesty; the packaged file list against the
|
CHANGELOG read for honesty; the packaged file list against the
|
||||||
package policy; the manifest sanity; **the freeze inventory read
|
package policy; the manifest sanity; **the freeze inventory read
|
||||||
against the landed code** — the list of surfaces the publish makes
|
against the landed code**, checking the list of surfaces the publish makes
|
||||||
one-way (the mapping contracts, the honest-advertisement posture,
|
one-way (the mapping contracts, the honest-advertisement posture,
|
||||||
the decline-matrix behavior, the fingerprint format, the russh
|
the decline-matrix behavior, the fingerprint format, the russh
|
||||||
pin's wire posture) confirmed accurate, because `w5-publish`
|
pin's wire posture) confirmed accurate, because `w5-publish`
|
||||||
executes immediately after this gate.
|
executes immediately after this gate.
|
||||||
- **The plan's closure conditions** — the scope-authority checklist
|
- **The plan's closure conditions**, verifying the scope-authority checklist
|
||||||
(the complete two-direction door shipped; the assembly out; no
|
(the complete two-direction door shipped; the assembly out; no
|
||||||
binary; single crate) verified against the landed state; the
|
binary; single crate) verified against the landed state; the
|
||||||
upstream asks (the wave-3 gate's item 5 lineage) verified still
|
upstream asks (the wave-3 gate's item 5 lineage) verified still
|
||||||
booked with their owners — nothing absorbed silently through the
|
booked with their owners, with nothing absorbed silently through the
|
||||||
release.
|
release.
|
||||||
|
|
||||||
Verdict: green (`w5-publish` unblocked) or findings booked
|
Verdict: green (`w5-publish` unblocked) or findings booked
|
||||||
@@ -80,27 +80,27 @@ Verdict: green (`w5-publish` unblocked) or findings booked
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
**Verdict: green.** `w5-publish` is unblocked. Every gate read was
|
**Verdict: green.** `w5-publish` is unblocked. Every gate read was
|
||||||
re-executed live at this commit (2026-10-30, post-`ab5236e`) — nothing
|
re-executed live at this commit (2026-10-30, post-`ab5236e`), and nothing
|
||||||
was taken solely from the task records. No blocking issues; no code
|
was taken solely from the task records. No blocking issues; no code
|
||||||
findings; nothing rose to booked-remediation level. Notes below.
|
findings; nothing rose to booked-remediation level. Notes below.
|
||||||
|
|
||||||
### The matrix suite ✓
|
### The matrix suite ✓
|
||||||
|
|
||||||
Every test name the coverage audit pins resolves in its binary (the
|
Every test name the coverage audit pins resolves in its binary (the
|
||||||
full inventory grep-verified this session — all 26 `dispatch_core`
|
full inventory grep-verified this session, with all 26 `dispatch_core`
|
||||||
rows / all 8 `tunnel_mapping` + `git_mapping` + client-side rows
|
rows / all 8 `tunnel_mapping` + `git_mapping` + client-side rows
|
||||||
named in the audit table exist), and `mapping.md` §Decline matrix
|
named in the audit table exist), and `mapping.md` §Decline matrix
|
||||||
traces cell by cell onto audit rows: the none build declines
|
traces cell by cell onto audit rows: the none build declines
|
||||||
everything (`every_channel_open_declines_in_a_default_build` + the
|
everything (`every_channel_open_declines_in_a_default_build` + the
|
||||||
shared x11/agent/cancel/listen rows); the git build's declines
|
shared x11/agent/cancel/listen rows); the git build's declines
|
||||||
(pty/shell — the gap-closed pty row, plain exec, tunnel opens,
|
(pty/shell including the gap-closed pty row, then plain exec, tunnel opens,
|
||||||
unreachable session requests, subsystems, unknown services) and its
|
unreachable session requests, subsystems, unknown services) and its
|
||||||
one serve (git-parseable exec); the session build's symmetric shape
|
one serve (git-parseable exec); the session build's symmetric shape
|
||||||
(git-parseable exec declines, wrapper-unset declines, seam-unset
|
(git-parseable exec declines, wrapper-unset declines, seam-unset
|
||||||
pty/shell); the tunnel build's serve/decline split incl. the seam-unset
|
pty/shell); the tunnel build's serve/decline split incl. the seam-unset
|
||||||
listen forms and the `ConnectFailed` fabric reject; the sftp slot's
|
listen forms and the `ConnectFailed` fabric reject; the sftp slot's
|
||||||
unchanged-underlying-build row (the subsystem row can't appear where
|
unchanged-underlying-build row (the subsystem row can't appear where
|
||||||
session channels decline at open — the audit row 8's reasoning holds;
|
session channels decline at open, so the audit row 8's reasoning holds;
|
||||||
the client-side placeholder pins every build); the never-features
|
the client-side placeholder pins every build); the never-features
|
||||||
pinned in every build both directions, with the `auth-agent` open's
|
pinned in every build both directions, with the `auth-agent` open's
|
||||||
pin-bound-unreachable record read and accepted (the russh-0.65
|
pin-bound-unreachable record read and accepted (the russh-0.65
|
||||||
@@ -108,25 +108,25 @@ pre-handler decline is part of the pin per ADR-002). The runner
|
|||||||
(`scripts/matrix.sh`, the script vehicle decided + recorded) is in
|
(`scripts/matrix.sh`, the script vehicle decided + recorded) is in
|
||||||
AGENTS.md's verification list.
|
AGENTS.md's verification list.
|
||||||
|
|
||||||
### Both e2e postures ✓ — nothing silently green
|
### Both e2e postures ✓, with nothing silently green
|
||||||
|
|
||||||
`tests/e2e_server.rs` (12 rows) and `tests/e2e_client.rs` (7 rows)
|
`tests/e2e_server.rs` (12 rows) and `tests/e2e_client.rs` (7 rows)
|
||||||
both read: every real-tool row is check-gated with
|
both read: every real-tool row is check-gated with
|
||||||
`support::skip(row, reason)` prints — tool absent/short, spawn
|
`support::skip(row, reason)` prints, covering a tool absent or short, spawn
|
||||||
refusal, and (client fixture) never-listening with the server's `-e`
|
refusal, and (client fixture) never-listening with the server's `-e`
|
||||||
log tail — never a silent pass; every row is deadline-bounded end to
|
log tail, so never a silent pass; every row is deadline-bounded end to
|
||||||
end; every decline row additionally pins the door-side decline
|
end; every decline row additionally pins the door-side decline
|
||||||
event (the e2e_server rows). The task records' findings are all
|
event (the e2e_server rows). The task records' findings are all
|
||||||
e2e-graph-local flake hardening (per-row workdirs, the monotonic
|
e2e-graph-local flake hardening (per-row workdirs, the monotonic
|
||||||
port allocator, the fabric settle, the event poll) — corroborated by
|
port allocator, the fabric settle, the event poll), corroborated by
|
||||||
the 10-consecutive-run claims and this session's full-matrix run.
|
the 10-consecutive-run claims and this session's full-matrix run.
|
||||||
The e2e_client fixture/gating decisions (tunnel+session gating for
|
The e2e_client fixture/gating decisions (tunnel+session gating for
|
||||||
the stays-usable row, every-build rows for the feature-free ops) are
|
the stays-usable row, every-build rows for the feature-free ops) are
|
||||||
recorded in the task and read honest. On this host every row ran
|
recorded in the task and read honest. On this host every row ran
|
||||||
(OpenSSH 9.6p1, git 2.43, curl 8.5, sftp present) — the matrix log
|
(OpenSSH 9.6p1, git 2.43, curl 8.5, sftp present), and the matrix log
|
||||||
carries the real-tool rows green, no skip prints fired.
|
carries the real-tool rows green, no skip prints fired.
|
||||||
|
|
||||||
### The fuzz sidecar ✓ — convention held, logs clean
|
### The fuzz sidecar ✓, with the convention held and logs clean
|
||||||
|
|
||||||
Layout matches the alkgit convention (`fuzz/README.md`: thin
|
Layout matches the alkgit convention (`fuzz/README.md`: thin
|
||||||
`fuzz_target!` wrappers, the stable-clean `shared/` library, the
|
`fuzz_target!` wrappers, the stable-clean `shared/` library, the
|
||||||
@@ -134,15 +134,15 @@ detached runner, the nightly pin inside `fuzz/`, committed seeds +
|
|||||||
the deterministic generator, grown corpora/artifacts/coverage
|
the deterministic generator, grown corpora/artifacts/coverage
|
||||||
gitignored). `gen_fuzz_seeds.py` re-run this session was a no-op
|
gitignored). `gen_fuzz_seeds.py` re-run this session was a no-op
|
||||||
diff on the committed set. The corpus gate re-ran green (11/11,
|
diff on the committed set. The corpus gate re-ran green (11/11,
|
||||||
exit 0). The four final campaign logs read clean — `oom/timeout/
|
exit 0). The four final campaign logs read clean: `oom/timeout/
|
||||||
crash: 0/0/0`, `exiting: 0` (exec_parse 43M+ execs / fingerprint
|
crash: 0/0/0`, `exiting: 0` (exec_parse 43M+ execs / fingerprint
|
||||||
12M+ / key_decode 5M+ / terminal_params 9M+); the two first-campaign
|
12M+ / key_decode 5M+ / terminal_params 9M+); the two first-campaign
|
||||||
crash logs (exec_parse exit 77, key_decode exit 77) are exactly the
|
crash logs (exec_parse exit 77, key_decode exit 77) are exactly the
|
||||||
recorded in-task findings — the harness oracle's double-`/` strip
|
recorded in-task findings, the harness oracle's double-`/` strip
|
||||||
(harness-side, fixed) and the ssh-key PPK header echo (the real
|
(harness-side, fixed) and the ssh-key PPK header echo (the real
|
||||||
finding: `decode_key_arm` now carries a crate-owned fixed message;
|
finding: `decode_key_arm` now carries a crate-owned fixed message;
|
||||||
re-ran clean). The exposure shape matches the record: one
|
re-ran clean). The exposure shape matches the record: one
|
||||||
no-exposure target (fingerprint — fully public ingress) + three
|
no-exposure target (fingerprint, with its fully public ingress) plus three
|
||||||
`#[doc(hidden)]` production-surface re-exports; the root crate's
|
`#[doc(hidden)]` production-surface re-exports; the root crate's
|
||||||
package excludes the sidecar (verified in the package list) and the
|
package excludes the sidecar (verified in the package list) and the
|
||||||
root workspace excludes it (workspace + package excludes in
|
root workspace excludes it (workspace + package excludes in
|
||||||
@@ -151,40 +151,40 @@ root workspace excludes it (workspace + package excludes in
|
|||||||
### Release readiness ✓
|
### Release readiness ✓
|
||||||
|
|
||||||
`cargo doc --no-deps` warning-free on the default build (matrix run)
|
`cargo doc --no-deps` warning-free on the default build (matrix run)
|
||||||
**and** `--all-features` — re-run this session, exit 0, zero
|
**and** `--all-features`, re-run this session with exit 0 and zero
|
||||||
warnings. README (feature graph + fail-closed posture + custody
|
warnings. README (feature graph + fail-closed posture + custody
|
||||||
summary + the doctest-mirrored examples) and the CHANGELOG 0.1.0
|
summary + the doctest-mirrored examples) and the CHANGELOG 0.1.0
|
||||||
founding entry both read honest and match the landed surface. The
|
founding entry both read honest and match the landed surface. The
|
||||||
publish dry-run re-ran clean (76 files packaged, verification
|
publish dry-run re-ran clean (76 files packaged, verification
|
||||||
compile passed, upload aborted by dry run); `cargo package --list`
|
compile passed, upload aborted by dry run); `cargo package --list`
|
||||||
re-verified against the package policy — exactly `src/` (20),
|
re-verified against the package policy: exactly `src/` (20),
|
||||||
`tests/` (+common +vectors), `docs/architecture/` (the full
|
`tests/` (+common +vectors), `docs/architecture/` (the full
|
||||||
contract: the four spec docs + ADRs 001–006 + README + the tracker),
|
contract: the four spec docs + ADRs 001–006 + README + the tracker),
|
||||||
`README`, `CHANGELOG`, both licenses, the manifests, and
|
`README`, `CHANGELOG`, both licenses, the manifests, and
|
||||||
`scripts/matrix.sh`; none of the excluded surface (AGENTS.md,
|
`scripts/matrix.sh`; none of the excluded surface (AGENTS.md,
|
||||||
`.opencode/`, plans/research/reviews/sdd_process, tasks/, `fuzz/`)
|
`.opencode/`, plans/research/reviews/sdd_process, tasks/, `fuzz/`)
|
||||||
appears. Manifest sanity: version 0.1.0 (the manifest value —
|
appears. Manifest sanity: version 0.1.0 (the manifest value;
|
||||||
`w5-publish` verifies, no bump), edition 2024, `rust-version` 1.89,
|
`w5-publish` verifies that no bump is needed), edition 2024, `rust-version` 1.89,
|
||||||
license + both files, the pins exactly as ADR-002 records them
|
license + both files, the pins exactly as ADR-002 records them
|
||||||
(russh 0.65 `default-features = false` + `aws-lc-rs`; alkcall 0.10;
|
(russh 0.65 `default-features = false` + `aws-lc-rs`; alkcall 0.10;
|
||||||
alkvault 0.1; the optional siblings 0.8.1 / 0.1.0 / 0.4.1), the
|
alkvault 0.1; the optional siblings 0.8.1 / 0.1.0 / 0.4.1), the
|
||||||
feature graph exactly ADR-003's flat shape, `default = []`.
|
feature graph exactly ADR-003's flat shape, `default = []`.
|
||||||
|
|
||||||
### The freeze inventory ✓ — accurate against the landed code
|
### The freeze inventory ✓, accurate against the landed code
|
||||||
|
|
||||||
Item by item: (1) the mapping contracts — the route-1 bridge,
|
Item by item: (1) the mapping contracts, i.e. the route-1 bridge,
|
||||||
exec-only grammar + verdicts, parse-only dispatch +
|
exec-only grammar + verdicts, parse-only dispatch +
|
||||||
decline-before-alloc, translation bridge + exit rules 1–6, the client
|
decline-before-alloc, translation bridge + exit rules 1–6, the client
|
||||||
planes — as executed in `dispatch.rs`/`tty_map.rs`/`git_map.rs`/
|
planes, as executed in `dispatch.rs`/`tty_map.rs`/`git_map.rs`/
|
||||||
`tunnel_map.rs`/`client_ops.rs` and line-by-line read green at the
|
`tunnel_map.rs`/`client_ops.rs` and line-by-line read green at the
|
||||||
wave-3/4 gates; (2) the honest-advertisement posture — `MethodSet`
|
wave-3/4 gates; (2) the honest-advertisement posture, where `MethodSet`
|
||||||
exactly `{PublicKey}` at the adapter's `Config` construction
|
exactly `{PublicKey}` at the adapter's `Config` construction
|
||||||
(`adapter.rs:135`), verified by its own unit row; (3) the
|
(`adapter.rs:135`), verified by its own unit row; (3) the
|
||||||
decline-matrix behavior — the `OpenDeclineReason` vocabulary and
|
decline-matrix behavior, the `OpenDeclineReason` vocabulary and
|
||||||
decline shapes as pinned by the matrix suite; (4) the fingerprint
|
decline shapes as pinned by the matrix suite; (4) the fingerprint
|
||||||
format — `SHA256:` + unpadded base64 via `HashAlg::Sha256`
|
format, `SHA256:` plus unpadded base64 via `HashAlg::Sha256`
|
||||||
(`fingerprint.rs:29`), the single path both the auth bridge and the
|
(`fingerprint.rs:29`), the single path both the auth bridge and the
|
||||||
policy pin consume; (5) the russh pin's wire posture — 0.65.0 +
|
policy pin consume; (5) the russh pin's wire posture, 0.65.0 plus
|
||||||
`aws-lc-rs`, `des`/`dsa` never, per `Cargo.toml`. The inventory as
|
`aws-lc-rs`, `des`/`dsa` never, per `Cargo.toml`. The inventory as
|
||||||
recorded in `w5-release-readiness.md` matches the landed code; the
|
recorded in `w5-release-readiness.md` matches the landed code; the
|
||||||
publish freezes exactly those surfaces.
|
publish freezes exactly those surfaces.
|
||||||
@@ -194,25 +194,25 @@ publish freezes exactly those surfaces.
|
|||||||
The complete two-direction door shipped (server stack + all four
|
The complete two-direction door shipped (server stack + all four
|
||||||
mapping rows + the decline placeholder; dial + op rows + the `-R`
|
mapping rows + the decline placeholder; dial + op rows + the `-R`
|
||||||
leg + lifecycle primitives); the assembly stays out (no
|
leg + lifecycle primitives); the assembly stays out (no
|
||||||
identity/ACL/fabric policy owned by the crate — seams only); no
|
identity/ACL/fabric policy owned by the crate, seams only); no
|
||||||
binary (no `[[bin]]` target; the package ships one lib); single
|
binary (no `[[bin]]` target; the package ships one lib); single
|
||||||
crate (single-member workspace, no tracks); nothing absorbed
|
crate (single-member workspace, no tracks); nothing absorbed
|
||||||
silently — the upstream-ask inventory verified still booked with its
|
silently, with the upstream-ask inventory verified still booked with its
|
||||||
owners:
|
owners:
|
||||||
|
|
||||||
- wave-3 gate item 5 (two asks): the alktunnels/alkcall
|
- wave-3 gate item 5 (two asks): the alktunnels/alkcall
|
||||||
non-detaching post-construction identity seam (the door keeps the
|
non-detaching post-construction identity seam (the door keeps the
|
||||||
no-detach posture — no `set_identity`-style surface appeared in
|
no-detach posture, and no `set_identity`-style surface appeared in
|
||||||
`src/` through the release), and mapping.md's correlation-miss
|
`src/` through the release), and mapping.md's correlation-miss
|
||||||
event-vocabulary nuance.
|
event-vocabulary nuance.
|
||||||
- The wave-5 fuzz finding's `ssh-key` PPK parse-echo ask — recorded
|
- The wave-5 fuzz finding's `ssh-key` PPK parse-echo ask, recorded
|
||||||
in `w5-fuzz-harness.md` as booked with the russh/ssh-key owners;
|
in `w5-fuzz-harness.md` as booked with the russh/ssh-key owners;
|
||||||
the actual tracker filing is owner-territory and stays listed
|
the actual tracker filing is owner-territory and stays listed
|
||||||
here so it cannot be absorbed at the publish.
|
here so it cannot be absorbed at the publish.
|
||||||
|
|
||||||
### Observations (non-blocking, no action required)
|
### Observations (non-blocking, no action required)
|
||||||
|
|
||||||
- The packaged file list includes `tests/vectors/alpha.key` — the
|
- The packaged file list includes `tests/vectors/alpha.key`, the
|
||||||
committed ed25519 test-fixture private key. Intentional (wave 1's
|
committed ed25519 test-fixture private key. Intentional (wave 1's
|
||||||
fixtures, public test material, no custody posture applies) and
|
fixtures, public test material, no custody posture applies) and
|
||||||
consistent across the family's sibling crates; noted here so the
|
consistent across the family's sibling crates; noted here so the
|
||||||
@@ -224,8 +224,8 @@ owners:
|
|||||||
### Verification (re-run at this commit, all green, 2026-10-30)
|
### Verification (re-run at this commit, all green, 2026-10-30)
|
||||||
|
|
||||||
`scripts/matrix.sh`: `cargo test` × 6 builds (none / git / session /
|
`scripts/matrix.sh`: `cargo test` × 6 builds (none / git / session /
|
||||||
tunnel / sftp-only / --all-features — 151 / 195 / 210 / 201 / 151 /
|
tunnel / sftp-only / --all-features, with 151 / 195 / 210 / 201 /
|
||||||
281 tests passed, 0 failed; all integration binaries + doctests
|
151 / 281 tests passed, 0 failed; all integration binaries + doctests
|
||||||
included) · clippy (--all-targets -D warnings) ✓ · clippy
|
included) · clippy (--all-targets -D warnings) ✓ · clippy
|
||||||
(--all-features --all-targets -D warnings) ✓ · `cargo fmt --check`
|
(--all-features --all-targets -D warnings) ✓ · `cargo fmt --check`
|
||||||
✓ · `cargo doc --no-deps` ✓ (plus `--all-features` re-run ✓) · the
|
✓ · `cargo doc --no-deps` ✓ (plus `--all-features` re-run ✓) · the
|
||||||
@@ -236,13 +236,13 @@ completion statuses).
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
**Verdict: green — `w5-publish` unblocked.** The wave-5 release gate
|
**Verdict: green, `w5-publish` unblocked.** The wave-5 release gate
|
||||||
re-executed every read live at the gate commit: the coverage audit
|
re-executed every read live at the gate commit: the coverage audit
|
||||||
traced cell by cell onto `mapping.md`'s decline-matrix table with
|
traced cell by cell onto `mapping.md`'s decline-matrix table with
|
||||||
every pinned test resolving in its binary and the `sftp`
|
every pinned test resolving in its binary and the `sftp`
|
||||||
no-op-feature fact + never-features verified in every build both
|
no-op-feature fact + never-features verified in every build both
|
||||||
directions; both e2e binaries read with nothing silently green (the
|
directions; both e2e binaries read with nothing silently green (the
|
||||||
whole suite ran on this host — no skips fired); the fuzz sidecar
|
whole suite ran on this host, so no skips fired); the fuzz sidecar
|
||||||
held the alkgit convention with the seeds no-op, the corpus gate
|
held the alkgit convention with the seeds no-op, the corpus gate
|
||||||
green, and all four final campaigns clean (the two first-campaign
|
green, and all four final campaigns clean (the two first-campaign
|
||||||
crashes exactly the recorded fixed findings); release readiness
|
crashes exactly the recorded fixed findings); release readiness
|
||||||
@@ -253,6 +253,6 @@ become one-way at the publish; the plan's closure conditions
|
|||||||
verified (two-direction door, assembly out, no binary, single
|
verified (two-direction door, assembly out, no binary, single
|
||||||
crate, upstream asks still booked). One real code fix was already
|
crate, upstream asks still booked). One real code fix was already
|
||||||
discharged in-task (the `ssh-key` PPK echo → crate-owned decode
|
discharged in-task (the `ssh-key` PPK echo → crate-owned decode
|
||||||
message). No blocking issues and no findings — this is the plan's
|
message). No blocking issues and no findings, and this is the plan's
|
||||||
last review; `w5-publish` (tag `alkssh-v0.1.0`, the verification
|
last review; `w5-publish` (tag `alkssh-v0.1.0`, the verification
|
||||||
set, the publish, the plan closure) closes it.
|
set, the publish, the plan closure) closes it.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w1-config
|
id: w1-config
|
||||||
name: Wave 1 — the config surface (serve-config, dial-config, Limits)
|
name: "Wave 1: the config surface (serve-config, dial-config, Limits)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w1-manifest, w1-key-sources, w1-host-key-policy]
|
depends_on: [w1-manifest, w1-key-sources, w1-host-key-policy]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -11,37 +11,38 @@ level: implementation
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the pure configuration types both directions implement against —
|
Land the pure configuration types both directions implement against,
|
||||||
the shapes every later wave's constructors take:
|
the shapes every later wave's constructors take:
|
||||||
|
|
||||||
- **Serve-config** (server direction, `mapping.md`/`overview.md`) —
|
- **Serve-config** (server direction, `mapping.md`/`overview.md`), the
|
||||||
the assembly-supplied serving configuration: the OQ-08 fields
|
assembly-supplied serving configuration: the OQ-08 fields
|
||||||
(**shell argv** and **exec wrapper argv**, each `Option<Vec<String>>`
|
(**shell argv** and **exec wrapper argv**, each `Option<Vec<String>>`;
|
||||||
— unset → its row declines with a clean error, never a fallback, no
|
when unset, its row declines with a clean error, never a fallback,
|
||||||
crate default; the wrapper form is platform-neutral), the
|
no crate default; the wrapper form is platform-neutral), the
|
||||||
`IdentityProvider` handle (`Arc<dyn IdentityProvider>` — the
|
`IdentityProvider` handle (`Arc<dyn IdentityProvider>`, which the
|
||||||
assembly injects it; the crate never resolves identities itself),
|
assembly injects; the crate never resolves identities itself), the
|
||||||
the host-key source handle (the wave-1 key-source seam), the tunnel
|
host-key source handle (the wave-1 key-source seam), the tunnel
|
||||||
`ChannelClient` handle (the route-1 bridge's family-fabric entry —
|
`ChannelClient` handle (the route-1 bridge's family-fabric entry,
|
||||||
`Option`, absent → tunnel rows decline even under the feature), and
|
held as an `Option`; when absent, tunnel rows decline even under
|
||||||
the `Limits`. `cwd`/`env` in `NegotiateRequest` are assembly-supplied
|
the feature), and the `Limits`. `cwd`/`env` in `NegotiateRequest`
|
||||||
serve-config values, never wire-derived (OQ-09 posture).
|
are assembly-supplied serve-config values, never wire-derived
|
||||||
- **Dial-config** (client direction, `client.md` §The dial surface) —
|
(OQ-09 posture).
|
||||||
address, key source (the wave-1 seam — never static keys, never env
|
- **Dial-config** (client direction, `client.md` §The dial surface):
|
||||||
|
address, key source (the wave-1 seam; never static keys, never env
|
||||||
vars), the `HostKeyPolicy` handle, and the bounded-resource knobs:
|
vars), the `HostKeyPolicy` handle, and the bounded-resource knobs:
|
||||||
`keepalive_interval`/`keepalive_max` (15 s / 3 default posture per
|
`keepalive_interval`/`keepalive_max` (15 s / 3 default posture per
|
||||||
the reverse-runner evidence), `inactivity_timeout`, window/packet
|
the reverse-runner evidence), `inactivity_timeout`, window/packet
|
||||||
knobs.
|
knobs.
|
||||||
- **`Limits`** — the bounded-resources budget model (`overview.md`'s
|
- **`Limits`**: the bounded-resources budget model (`overview.md`'s
|
||||||
table): the in-crate max-concurrent-channels cap, exec-parse length
|
table): the in-crate max-concurrent-channels cap, exec-parse length
|
||||||
caps (consumed by the wave-3 grammar), and the russh knob mapping
|
caps (consumed by the wave-3 grammar), and the russh knob mapping
|
||||||
with documented default postures — `max_auth_attempts` default 10
|
with documented default postures (`max_auth_attempts` default 10,
|
||||||
(sized for the counting `none`-probe, ADR-004 decision 6),
|
sized for the counting `none`-probe per ADR-004 decision 6;
|
||||||
`window_size`/`maximum_packet_size`/`channel_buffer_size` set,
|
`window_size`/`maximum_packet_size`/`channel_buffer_size` set;
|
||||||
`inactivity_timeout` set (assembly-tunable). Every session carries
|
`inactivity_timeout` set, assembly-tunable). Every session carries
|
||||||
it; no unbounded loops, no unbounded buffers.
|
it; no unbounded loops, no unbounded buffers.
|
||||||
|
|
||||||
Types only — no russh `Config` construction (wave 2), no dialing
|
Types only: no russh `Config` construction (wave 2), no dialing
|
||||||
(wave 4). The OQ-08 unset-declines semantics live in the type shape
|
(wave 4). The OQ-08 unset-declines semantics live in the type shape
|
||||||
(`Option` fields + doc contracts), not in construction-time
|
(`Option` fields + doc contracts), not in construction-time
|
||||||
validation.
|
validation.
|
||||||
@@ -71,7 +72,7 @@ validation.
|
|||||||
section)
|
section)
|
||||||
- docs/architecture/client.md §The dial surface
|
- docs/architecture/client.md §The dial surface
|
||||||
- docs/architecture/open-questions.md (OQ-08, OQ-09 resolutions)
|
- docs/architecture/open-questions.md (OQ-08, OQ-09 resolutions)
|
||||||
- docs/architecture/decisions/004-auth-dispositions.md (decision 6 —
|
- docs/architecture/decisions/004-auth-dispositions.md (decision 6:
|
||||||
the `max_auth_attempts` default)
|
the `max_auth_attempts` default)
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
@@ -82,25 +83,25 @@ validation.
|
|||||||
>
|
>
|
||||||
> - **`Limits` is the single budget model both directions carry**
|
> - **`Limits` is the single budget model both directions carry**
|
||||||
> (`Copy`, plain tuning data): the in-crate caps
|
> (`Copy`, plain tuning data): the in-crate caps
|
||||||
> (`max_concurrent_channels` 64, `max_exec_command_len` 8192 —
|
> (`max_concurrent_channels` 64, `max_exec_command_len` 8192, with
|
||||||
> numbers chosen here, documented as assembly-tunable posture
|
> numbers chosen here and documented as assembly-tunable posture
|
||||||
> constants) plus the russh-knob mapping (`max_auth_attempts` 10
|
> constants) plus the russh-knob mapping (`max_auth_attempts` 10
|
||||||
> per ADR-004 decision 6; `window_size` 2 MiB / `maximum_packet_size`
|
> per ADR-004 decision 6; `window_size` 2 MiB / `maximum_packet_size`
|
||||||
> 32 KiB / `channel_buffer_size` 100 — russh's own defaults, kept;
|
> 32 KiB / `channel_buffer_size` 100, russh's own defaults kept;
|
||||||
> `inactivity_timeout` 600 s carried as plain `Duration` so the
|
> `inactivity_timeout` 600 s carried as plain `Duration` so the
|
||||||
> "set (assembly-tunable)" posture is structural — wave 2/4 map it
|
> "set (assembly-tunable)" posture is structural, with wave 2/4
|
||||||
> to russh's `Option` as always-`Some`;
|
> mapping it to russh's `Option` as always-`Some`;
|
||||||
> `keepalive_interval`/`keepalive_max` Some(15 s)/3 riding inside as
|
> `keepalive_interval`/`keepalive_max` Some(15 s)/3 riding inside as
|
||||||
> the client-direction posture, satisfying the dial-config
|
> the client-direction posture, satisfying the dial-config
|
||||||
> criterion without a duplicate knob set). russh's rekey `Limits`
|
> criterion without a duplicate knob set). russh's rekey `Limits`
|
||||||
> stay russh-enforced at their RFC-4253 defaults and are not
|
> stay russh-enforced at their RFC-4253 defaults and are not
|
||||||
> carried (documented; wave-2 material).
|
> carried (documented; wave-2 material).
|
||||||
> - **Dial-config carries the knobs via its `Limits` field** — the
|
> - **Dial-config carries the knobs via its `Limits` field**: the
|
||||||
> criterion's "keepalive/inactivity/window knobs with the
|
> criterion's "keepalive/inactivity/window knobs with the
|
||||||
> documented defaults" read as the budget model riding the config;
|
> documented defaults" read as the budget model riding the config;
|
||||||
> the accepted criterion on `Limits` itself pins that placement,
|
> the accepted criterion on `Limits` itself pins that placement,
|
||||||
> so a second knob set on `DialConfig` would have duplicated state.
|
> so a second knob set on `DialConfig` would have duplicated state.
|
||||||
> - **No `Default` for the two configs** — the required handles
|
> - **No `Default` for the two configs**: the required handles
|
||||||
> (`Arc<dyn IdentityProvider>`,
|
> (`Arc<dyn IdentityProvider>`,
|
||||||
> `Arc<dyn HostKeySource>`, `Arc<dyn KeySource>`,
|
> `Arc<dyn HostKeySource>`, `Arc<dyn KeySource>`,
|
||||||
> `Arc<dyn HostKeyPolicy>`) have no crate-defaultable shape
|
> `Arc<dyn HostKeyPolicy>`) have no crate-defaultable shape
|
||||||
@@ -111,12 +112,12 @@ validation.
|
|||||||
> + doc contracts**; no construction-time validation (the
|
> + doc contracts**; no construction-time validation (the
|
||||||
> `error.rs` no-config-error contract). Two request-time check
|
> `error.rs` no-config-error contract). Two request-time check
|
||||||
> helpers exist (`shell_configured`/`exec_wrapper_configured`,
|
> helpers exist (`shell_configured`/`exec_wrapper_configured`,
|
||||||
> plus `tunnel_configured`) for the wave-2 dispatch to call —
|
> plus `tunnel_configured`) for the wave-2 dispatch to call,
|
||||||
> reading the `Option`, never validating.
|
> reading the `Option`, never validating.
|
||||||
> - **`cwd`/`env` (OQ-09 posture) carried on `ServeConfig`** as
|
> - **`cwd`/`env` (OQ-09 posture) carried on `ServeConfig`** as
|
||||||
> `Option<PathBuf>` / `BTreeMap<String, String>` (empty defaults;
|
> `Option<PathBuf>` / `BTreeMap<String, String>` (empty defaults;
|
||||||
> never wire-derived — doc contract).
|
> never wire-derived; doc contract).
|
||||||
> - **`channel_client` is not feature-gated** — alkcall is a base
|
> - **`channel_client` is not feature-gated**: alkcall is a base
|
||||||
> dependency and a field presence is not a capability; the `tunnel`
|
> dependency and a field presence is not a capability; the `tunnel`
|
||||||
> feature gate applies at wave 2 on top (absent → decline even
|
> feature gate applies at wave 2 on top (absent → decline even
|
||||||
> under the feature; unfeatured → decline too). Doc contract
|
> under the feature; unfeatured → decline too). Doc contract
|
||||||
@@ -124,7 +125,7 @@ validation.
|
|||||||
> - Verified the alkcall 0.10 surfaces in-source (registry):
|
> - Verified the alkcall 0.10 surfaces in-source (registry):
|
||||||
> `core::auth::IdentityProvider` is a sync,
|
> `core::auth::IdentityProvider` is a sync,
|
||||||
> `Send + Sync + 'static` trait (dyn-compatible with no
|
> `Send + Sync + 'static` trait (dyn-compatible with no
|
||||||
> async-trait help on alkssh's side — `ChannelClient` and the
|
> async-trait help on alkssh's side; `ChannelClient` and the
|
||||||
> `NoopIdentityProvider` test fixture ride
|
> `NoopIdentityProvider` test fixture ride
|
||||||
> `alkcall::channels::client` / `alkcall::core::auth`), and the
|
> `alkcall::channels::client` / `alkcall::core::auth`), and the
|
||||||
> russh 0.65.0 knob names (`window_size`, `maximum_packet_size`,
|
> russh 0.65.0 knob names (`window_size`, `maximum_packet_size`,
|
||||||
@@ -137,7 +138,8 @@ validation.
|
|||||||
> Config surface landed: `src/config.rs` with `Limits` (in-crate
|
> Config surface landed: `src/config.rs` with `Limits` (in-crate
|
||||||
> caps: `max_concurrent_channels` 64, `max_exec_command_len` 8192;
|
> caps: `max_concurrent_channels` 64, `max_exec_command_len` 8192;
|
||||||
> russh knobs: `max_auth_attempts` 10, window 2 MiB, packet 32 KiB,
|
> russh knobs: `max_auth_attempts` 10, window 2 MiB, packet 32 KiB,
|
||||||
> buffer 100, inactivity 600 s, keepalive Some(15 s)/3 — every number
|
> buffer 100, inactivity 600 s, keepalive Some(15 s)/3, with every
|
||||||
|
> number
|
||||||
> a named posture constant or documented ADR anchor), `ServeConfig`
|
> a named posture constant or documented ADR anchor), `ServeConfig`
|
||||||
> (OQ-08 `shell_argv`/`exec_wrapper_argv` as unset-declines
|
> (OQ-08 `shell_argv`/`exec_wrapper_argv` as unset-declines
|
||||||
> `Option<Vec<String>>`, OQ-09 `session_cwd`/`session_env`,
|
> `Option<Vec<String>>`, OQ-09 `session_cwd`/`session_env`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w1-error-model
|
id: w1-error-model
|
||||||
name: Wave 1 — the error model (fail-closed taxonomy)
|
name: "Wave 1: the error model (fail-closed taxonomy)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w1-manifest]
|
depends_on: [w1-manifest]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -12,25 +12,25 @@ level: implementation
|
|||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the crate's error taxonomy in `src/error.rs` (one module per file,
|
Land the crate's error taxonomy in `src/error.rs` (one module per file,
|
||||||
re-exported from the crate root — the alktty pattern), `thiserror`
|
re-exported from the crate root, the alktty pattern), `thiserror`
|
||||||
throughout, no catch-alls anywhere (AGENTS.md convention 2):
|
throughout, no catch-alls anywhere (AGENTS.md convention 2):
|
||||||
|
|
||||||
- **`KeySourceError`** — the ADR-005 decision-4 failure matrix as
|
- **`KeySourceError`**: the ADR-005 decision-4 failure matrix as
|
||||||
distinct variants, the vault's error taxonomy reshaped by the
|
distinct variants, the vault's error taxonomy reshaped by the
|
||||||
adapter, **never collapsed**: `Locked` (vault locked → no dial, and
|
adapter, **never collapsed**: `Locked` (vault locked → no dial, and
|
||||||
no server: a host key that can't load prevents server
|
no server: a host key that can't load prevents server
|
||||||
construction), `Absent` (no blob at the requested path),
|
construction), `Absent` (no blob at the requested path),
|
||||||
`Decryption` (wrong mnemonic / tampered ciphertext — the
|
`Decryption` (wrong mnemonic / tampered ciphertext: the
|
||||||
refuse-at-crypto class), `UnknownKeyVersion` (the vault's
|
refuse-at-crypto class), `UnknownKeyVersion` (the vault's
|
||||||
refuse-before-crypto `InvalidPath` class), `Decode` (plaintext
|
refuse-before-crypto `InvalidPath` class), `Decode` (plaintext
|
||||||
arrived but is not decodable key material — the one-time
|
arrived but is not decodable key material: the one-time
|
||||||
`decode_secret_key` failure). Doc comments carry the ADR semantics
|
`decode_secret_key` failure). Doc comments carry the ADR semantics
|
||||||
per variant, including the fail-closed consequence of each.
|
per variant, including the fail-closed consequence of each.
|
||||||
- Deliberately **not** error types: auth declines (dispositions —
|
- Deliberately **not** error types: auth declines (dispositions, the
|
||||||
`Auth::Reject`, ADR-004; never errors), wire decline reasons
|
`Auth::Reject` of ADR-004; never errors), wire decline reasons
|
||||||
(SSH-shaped responses — wave-2 material), config validation (the
|
(SSH-shaped responses, wave-2 material), config validation (the
|
||||||
OQ-08 fields are `Option` and decline at request time, not
|
OQ-08 fields are `Option` and decline at request time, not
|
||||||
construction time — no config-error type ships unless the config
|
construction time; no config-error type ships unless the config
|
||||||
task finds one needed, flagged for the gate).
|
task finds one needed, flagged for the gate).
|
||||||
|
|
||||||
The exact variant payload shapes are the task's to pin (minimal per
|
The exact variant payload shapes are the task's to pin (minimal per
|
||||||
@@ -57,27 +57,27 @@ message is the honest content), flagged for the wave-1 gate.
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
> Payload shapes pinned (flagged for the wave-1 gate): `Locked` and
|
> Payload shapes pinned (flagged for the wave-1 gate): `Locked` and
|
||||||
> `Absent` are payload-free — no honest content beyond the variant
|
> `Absent` are payload-free, with no honest content beyond the
|
||||||
> itself (the caller knows which source it requested);
|
> variant itself (the caller knows which source it requested);
|
||||||
> `Decryption(String)` and `UnknownKeyVersion(String)` carry the
|
> `Decryption(String)` and `UnknownKeyVersion(String)` carry the
|
||||||
> vault's opaque messages (`VaultServiceError::Encryption` /
|
> vault's opaque messages (`VaultServiceError::Encryption` /
|
||||||
> `InvalidPath` arrive as strings per POC-2 §3 — string is the honest
|
> `InvalidPath` arrive as strings per POC-2 §3, where a string is the
|
||||||
> content). `Decode` carries `String` rather than the typed
|
> honest content). `Decode` carries `String` rather than the typed
|
||||||
> `ssh_key::Error` (`russh::keys::decode_secret_key`'s error type):
|
> `ssh_key::Error` (`russh::keys::decode_secret_key`'s error type):
|
||||||
> the ssh-key RC type is deliberately kept out of the published
|
> the ssh-key RC type is deliberately kept out of the published
|
||||||
> public API — when the RC settles the payload can be re-typed, but
|
> public API; when the RC settles the payload can be re-typed, but
|
||||||
> freezing an RC error type now would be a worse one-way door. The
|
> freezing an RC error type now would be a worse one-way door. The
|
||||||
> module doc comment records the deliberately-not-error classes
|
> module doc comment records the deliberately-not-error classes
|
||||||
> (auth declines = dispositions per ADR-004; wire decline reasons =
|
> (auth declines = dispositions per ADR-004; wire decline reasons =
|
||||||
> wave-2; config validation = none unless w1-config finds a need —
|
> wave-2; config validation = none unless w1-config finds a need,
|
||||||
> the OQ-08 fields decline at request time). No `From<VaultServiceError>`
|
> as the OQ-08 fields decline at request time). No `From<VaultServiceError>`
|
||||||
> blanket impl on purpose: alkvault's taxonomy has variants
|
> blanket impl on purpose: alkvault's taxonomy has variants
|
||||||
> (`Mnemonic`, `Derivation`, `AlreadyUnlocked`, `UnsupportedKeyType`)
|
> (`Mnemonic`, `Derivation`, `AlreadyUnlocked`, `UnsupportedKeyType`)
|
||||||
> that do not map onto the five without collapsing, and the ADR's
|
> that do not map onto the five without collapsing, and the ADR's
|
||||||
> reshape-never-collapse rule kills the conversion; w1-key-sources
|
> reshape-never-collapse rule kills the conversion; w1-key-sources
|
||||||
> maps explicitly at its seam. The module stays engine-blind — no
|
> maps explicitly at its seam. The module stays engine-blind: no
|
||||||
> alkvault or russh types referenced at the error layer. Tests: an
|
> alkvault or russh types referenced at the error layer. Tests: an
|
||||||
> exhaustive `match` (no wildcard — the closed-variant invariant is
|
> exhaustive `match` (no wildcard, so the closed-variant invariant is
|
||||||
> compile-time enforced) plus `Display` string assertions over all
|
> compile-time enforced) plus `Display` string assertions over all
|
||||||
> five variants, `Debug` output, and `Clone` payload carrying.
|
> five variants, `Debug` output, and `Clone` payload carrying.
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ message is the honest content), flagged for the wave-1 gate.
|
|||||||
> fail-closed consequence (locked → no dial + no server; absent/decryption/
|
> fail-closed consequence (locked → no dial + no server; absent/decryption/
|
||||||
> unknown-version/decode → no dial; host-key path → no server
|
> unknown-version/decode → no dial; host-key path → no server
|
||||||
> construction throughout). Re-exported from the crate root
|
> construction throughout). Re-exported from the crate root
|
||||||
> (`mod error; pub use error::KeySourceError;` — the alktty pattern).
|
> (`mod error; pub use error::KeySourceError;`, the alktty pattern).
|
||||||
> No catch-all, no `Other/Unknown`, no blanket `From` from the vault's
|
> No catch-all, no `Other/Unknown`, no blanket `From` from the vault's
|
||||||
> taxonomy. Four unit tests (exhaustive match over the enum,
|
> taxonomy. Four unit tests (exhaustive match over the enum,
|
||||||
> `Display`, `Debug`, `Clone`). Verified: `cargo test` (4/4 + doc
|
> `Display`, `Debug`, `Clone`). Verified: `cargo test` (4/4 + doc
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w1-fingerprint
|
id: w1-fingerprint
|
||||||
name: Wave 1 — fingerprint computation (the contract invariant)
|
name: "Wave 1: fingerprint computation (the contract invariant)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w1-manifest]
|
depends_on: [w1-manifest]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -12,23 +12,23 @@ level: implementation
|
|||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the fingerprint computation the auth bridge (server) and the
|
Land the fingerprint computation the auth bridge (server) and the
|
||||||
host-key policy path (client) both consume — ADR-004 decision 1's
|
host-key policy path (client) both consume; this is ADR-004
|
||||||
contract invariant in code:
|
decision 1's contract invariant in code:
|
||||||
|
|
||||||
- Compute a key's fingerprint via the `ssh-key` re-export
|
- Compute a key's fingerprint via the `ssh-key` re-export
|
||||||
(`HashAlg::Sha256`): the `SHA256:` + **unpadded url-safe base64**
|
(`HashAlg::Sha256`): the `SHA256:` + **unpadded url-safe base64**
|
||||||
OpenSSH format. The string format is a **contract invariant** —
|
OpenSSH format. The string format is a **contract invariant**:
|
||||||
registration strings from any standard issuer (`ssh-keygen -lf`,
|
registration strings from any standard issuer (`ssh-keygen -lf`,
|
||||||
alkcall-side computation) must compare equal to what this crate
|
alkcall-side computation) must compare equal to what this crate
|
||||||
computes; parity is proven, not assumed (POC-1 §1).
|
computes; parity is proven, not assumed (POC-1 §1).
|
||||||
- The function takes the russh/ssh-key public-key type and returns the
|
- The function takes the russh/ssh-key public-key type and returns the
|
||||||
string; it is the single fingerprint path in the crate (no
|
string; it is the single fingerprint path in the crate (no
|
||||||
second formatting anywhere — a drift here breaks every identity
|
second formatting anywhere, since a drift here breaks every
|
||||||
resolution silently).
|
identity resolution silently).
|
||||||
- Parity tests against independent-issuer vectors: static ed25519
|
- Parity tests against independent-issuer vectors: static ed25519
|
||||||
test keys with their `ssh-keygen -lf`-recorded expected fingerprints
|
test keys with their `ssh-keygen -lf`-recorded expected fingerprints
|
||||||
(the POC-1 §1 checkpoint shape). The vectors are committed test
|
(the POC-1 §1 checkpoint shape). The vectors are committed test
|
||||||
fixtures (throwaway test keys — no secret material).
|
fixtures (throwaway test keys; no secret material).
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ contract invariant in code:
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- docs/architecture/decisions/004-auth-dispositions.md (decision 1 —
|
- docs/architecture/decisions/004-auth-dispositions.md (decision 1:
|
||||||
the format invariant)
|
the format invariant)
|
||||||
- docs/architecture/auth.md §The bridge (step 2)
|
- docs/architecture/auth.md §The bridge (step 2)
|
||||||
- docs/research/poc-1-auth-bridge.md §1 (the parity checkpoint)
|
- docs/research/poc-1-auth-bridge.md §1 (the parity checkpoint)
|
||||||
@@ -51,7 +51,7 @@ contract invariant in code:
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
> Design: `src/fingerprint.rs` exposes `fingerprint(&public_key) ->
|
> Design: `src/fingerprint.rs` exposes `fingerprint(&public_key) ->
|
||||||
> String` — the thin wrapper over the `ssh-key` re-export
|
> String`, the thin wrapper over the `ssh-key` re-export
|
||||||
> (`key.fingerprint(HashAlg::Sha256).to_string()`; russh 0.65
|
> (`key.fingerprint(HashAlg::Sha256).to_string()`; russh 0.65
|
||||||
> re-exports `ssh_key::{PublicKey, HashAlg}` via `russh::keys`), so
|
> re-exports `ssh_key::{PublicKey, HashAlg}` via `russh::keys`), so
|
||||||
> the *computation and formatting stay ssh-key's own code* and parity
|
> the *computation and formatting stay ssh-key's own code* and parity
|
||||||
@@ -59,14 +59,14 @@ contract invariant in code:
|
|||||||
> `alkssh::fingerprint`; a grep over `src/` confirms no second
|
> `alkssh::fingerprint`; a grep over `src/` confirms no second
|
||||||
> formatting path (doc mentions only). The russh 0.65 handler shape
|
> formatting path (doc mentions only). The russh 0.65 handler shape
|
||||||
> feeds exactly this type: `VerifiedPublicKey.public_key: &
|
> feeds exactly this type: `VerifiedPublicKey.public_key: &
|
||||||
> ssh_key::PublicKey` — the auth bridge (wave 2, w1-host-key-policy)
|
> ssh_key::PublicKey`, with the auth bridge (wave 2,
|
||||||
> passes that field straight through.
|
> w1-host-key-policy) passing that field straight through.
|
||||||
>
|
>
|
||||||
> **Fact-check finding (doc correction landed):** the ADR-004 /
|
> **Fact-check finding (doc correction landed):** the ADR-004 /
|
||||||
> auth.md / task wording "unpadded **url-safe** base64" is a misnomer.
|
> auth.md / task wording "unpadded **url-safe** base64" is a misnomer.
|
||||||
> The ssh-key crate's `Fingerprint` Display emits `SHA256:` +
|
> The ssh-key crate's `Fingerprint` Display emits `SHA256:` +
|
||||||
> *unpadded standard* base64 (base64ct `Base64Unpadded`
|
> *unpadded standard* base64 (base64ct `Base64Unpadded`
|
||||||
> = the standard `+/` alphabet), and OpenSSH does the same — the
|
> = the standard `+/` alphabet), and OpenSSH does the same, as the
|
||||||
> committed `ssh-keygen -lf` vectors contain `+` and `/`. The binding
|
> committed `ssh-keygen -lf` vectors contain `+` and `/`. The binding
|
||||||
> invariant was never "url-safe"; it is byte-parity with standard
|
> invariant was never "url-safe"; it is byte-parity with standard
|
||||||
> issuers, which POC-1 proved and the committed vectors now pin.
|
> issuers, which POC-1 proved and the committed vectors now pin.
|
||||||
@@ -85,7 +85,7 @@ contract invariant in code:
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
> Landed `src/fingerprint.rs` — the crate's single fingerprint path
|
> Landed `src/fingerprint.rs`, the crate's single fingerprint path
|
||||||
> (`alkssh::fingerprint`), delegating computation/formatting to the
|
> (`alkssh::fingerprint`), delegating computation/formatting to the
|
||||||
> ssh-key re-export behind russh 0.65's keys module with
|
> ssh-key re-export behind russh 0.65's keys module with
|
||||||
> `HashAlg::Sha256`. Parity proven, not assumed: 4 committed
|
> `HashAlg::Sha256`. Parity proven, not assumed: 4 committed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w1-host-key-policy
|
id: w1-host-key-policy
|
||||||
name: Wave 1 — the HostKeyPolicy seam (OQ-05, policy-blind)
|
name: "Wave 1: the HostKeyPolicy seam (OQ-05, policy-blind)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w1-manifest]
|
depends_on: [w1-manifest]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -12,27 +12,28 @@ level: implementation
|
|||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the client-side host-key policy seam (`auth.md` §Host-key policy,
|
Land the client-side host-key policy seam (`auth.md` §Host-key policy,
|
||||||
OQ-05 resolution) — the trust-decision contract the dial surface calls:
|
OQ-05 resolution), the trust-decision contract the dial surface
|
||||||
|
calls:
|
||||||
|
|
||||||
- **The `HostKeyPolicy` trait** — the crate surfaces the presented host
|
- **The `HostKeyPolicy` trait**: the crate surfaces the presented host
|
||||||
key (host, port, fingerprint) to an assembly-provided policy that
|
key (host, port, fingerprint) to an assembly-provided policy that
|
||||||
decides accept/reject. The two OQ-05 cases are the *implementor's*:
|
decides accept/reject. The two OQ-05 cases are the *implementor's*:
|
||||||
case-1 pins (known beforehand — hard-decline on mismatch) and case-2
|
case-1 pins (known beforehand, hard-decline on mismatch) and case-2
|
||||||
policies (unknown beforehand — prompt-and-record, learn-and-record,
|
policies (unknown beforehand: prompt-and-record, learn-and-record,
|
||||||
provider-metadata match, accept-and-continue for ephemeral fleets).
|
provider-metadata match, accept-and-continue for ephemeral fleets).
|
||||||
The crate is **policy-blind**: it never decides trust, never
|
The crate is **policy-blind**: it never decides trust, never
|
||||||
hard-codes a trust model.
|
hard-codes a trust model.
|
||||||
- **The crate default is fail-closed**: reject unknown (russh's own
|
- **The crate default is fail-closed**: reject unknown (russh's own
|
||||||
reject-all default, kept as the built-in default policy). **No
|
reject-all default, kept as the built-in default policy). **No
|
||||||
accept-all built-in crate option** — a permissive case-2 policy is
|
accept-all built-in crate option**: a permissive case-2 policy is
|
||||||
the assembly's to write.
|
the assembly's to write.
|
||||||
- The trait receives the fingerprint string (the wave-1 fingerprint
|
- The trait receives the fingerprint string (the wave-1 fingerprint
|
||||||
computation is the caller's input path, wave-4 dial); the exact
|
computation is the caller's input path, wave-4 dial); the exact
|
||||||
decision type (bool vs a richer verdict carrying a reason) is the
|
decision type (bool vs a richer verdict carrying a reason) is the
|
||||||
task's to pin against the russh `check_server_key` hook shape at
|
task's to pin against the russh `check_server_key` hook shape at
|
||||||
this pin — flagged for the wave-1 gate.
|
this pin (flagged for the wave-1 gate).
|
||||||
- The russh client-handler consumption (`check_server_key` wiring) is
|
- The russh client-handler consumption (`check_server_key` wiring) is
|
||||||
wave-4 dial-surface material — this task lands the seam and its
|
wave-4 dial-surface material; this task lands the seam and its
|
||||||
default only.
|
default only.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
@@ -52,7 +53,7 @@ OQ-05 resolution) — the trust-decision contract the dial surface calls:
|
|||||||
- docs/architecture/auth.md §Host-key policy (OQ-05 resolution)
|
- docs/architecture/auth.md §Host-key policy (OQ-05 resolution)
|
||||||
- docs/architecture/client.md §The dial surface, §What the crate does
|
- docs/architecture/client.md §The dial surface, §What the crate does
|
||||||
not include (trust decisions)
|
not include (trust decisions)
|
||||||
- docs/architecture/decisions/005-at-rest-key-custody.md (decision 6 —
|
- docs/architecture/decisions/005-at-rest-key-custody.md (decision 6:
|
||||||
the client host-*pin* is public config, never vault material)
|
the client host-*pin* is public config, never vault material)
|
||||||
- docs/research/auth-bridge.md §Host keys and `check_server_key`
|
- docs/research/auth-bridge.md §Host keys and `check_server_key`
|
||||||
|
|
||||||
@@ -62,11 +63,11 @@ OQ-05 resolution) — the trust-decision contract the dial surface calls:
|
|||||||
> `RejectAll`) + lib.rs re-exports. Design decisions pinned against
|
> `RejectAll`) + lib.rs re-exports. Design decisions pinned against
|
||||||
> the russh 0.65 hook shape (`Handler::check_server_key ->
|
> the russh 0.65 hook shape (`Handler::check_server_key ->
|
||||||
> Result<bool, Self::Error>`, async via `impl Future`, default
|
> Result<bool, Self::Error>`, async via `impl Future`, default
|
||||||
> `Ok(false)` — verified in the russh 0.65.0 source):
|
> `Ok(false)`, verified in the russh 0.65.0 source):
|
||||||
>
|
>
|
||||||
> - **The verdict type is `bool`** (the flagged pin): the hook's bool
|
> - **The verdict type is `bool`** (the flagged pin): the hook's bool
|
||||||
> is the trust decision and its `Err` channel is the handler's
|
> is the trust decision and its `Err` channel is the handler's
|
||||||
> wire/IO-failure territory — a trust decline has no typed reason
|
> wire/IO-failure territory; a trust decline has no typed reason
|
||||||
> channel on this surface. A decline is a disposition, not an
|
> channel on this surface. A decline is a disposition, not an
|
||||||
> error (`src/error.rs` contract); decline reasons are the
|
> error (`src/error.rs` contract); decline reasons are the
|
||||||
> policy's own record (case-2 policies log their decisions), not a
|
> policy's own record (case-2 policies log their decisions), not a
|
||||||
@@ -75,14 +76,14 @@ OQ-05 resolution) — the trust-decision contract the dial surface calls:
|
|||||||
> per auth.md).
|
> per auth.md).
|
||||||
> - **The trait is async and dyn-compatible**: `#[async_trait]`,
|
> - **The trait is async and dyn-compatible**: `#[async_trait]`,
|
||||||
> `decide(&self, &PresentedHostKey) -> bool`, `Send + Sync +
|
> `decide(&self, &PresentedHostKey) -> bool`, `Send + Sync +
|
||||||
> 'static` — the family injected-handle pattern (`Arc<dyn
|
> 'static`, the family injected-handle pattern (`Arc<dyn
|
||||||
> HostKeyPolicy>`, the alkcall `IdentityProvider`/`OperationEnv`
|
> HostKeyPolicy>`, the alkcall `IdentityProvider`/`OperationEnv`
|
||||||
> precedent), which w1-config's dial-config handle and
|
> precedent), which w1-config's dial-config handle and
|
||||||
> w1-test-doubles' inspectable double both require. State-holding
|
> w1-test-doubles' inspectable double both require. State-holding
|
||||||
> (learn-and-record) policies record by interior mutability
|
> (learn-and-record) policies record by interior mutability
|
||||||
> behind the shared `&self` handle.
|
> behind the shared `&self` handle.
|
||||||
> - **New dep `async-trait = "0.1"`** (MIT OR Apache-2.0; the only
|
> - **New dep `async-trait = "0.1"`** (MIT OR Apache-2.0; the only
|
||||||
> way to get a dyn-compatible async trait at this toolchain —
|
> way to get a dyn-compatible async trait at this toolchain, as
|
||||||
> native AFIT is not object-safe, and russh 0.65 removed its own
|
> native AFIT is not object-safe, and russh 0.65 removed its own
|
||||||
> `async-trait` feature, per ADR-002's context). Precedented and
|
> `async-trait` feature, per ADR-002's context). Precedented and
|
||||||
> deduped: alkcall already carries `async-trait 0.1`, so the tree
|
> deduped: alkcall already carries `async-trait 0.1`, so the tree
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w1-key-sources
|
id: w1-key-sources
|
||||||
name: Wave 1 — vault-backed key sources (ADR-005 custody in code)
|
name: "Wave 1: vault-backed key sources (ADR-005 custody in code)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w1-manifest, w1-error-model]
|
depends_on: [w1-manifest, w1-error-model]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -11,33 +11,33 @@ level: implementation
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the ADR-005 custody model in code — the key-source seam both
|
Land the ADR-005 custody model in code: the key-source seam both
|
||||||
directions consume (client dial keys, server host keys):
|
directions consume (client dial keys, server host keys):
|
||||||
|
|
||||||
- **The source seam** — the adapter takes *key sources*, not static
|
- **The source seam**: the adapter takes *key sources*, not static
|
||||||
keys (the alkhttp ADR-014 pattern): a source trait the assembly
|
keys (the alkhttp ADR-014 pattern): a source trait the assembly
|
||||||
implements over an injected alkvault handle, `EncryptedData` opaque
|
implements over an injected alkvault handle, `EncryptedData` opaque
|
||||||
to the crate (ADR-005 decision 5 — the crate never interprets
|
to the crate (ADR-005 decision 5: the crate never interprets
|
||||||
`key_version` or record internals).
|
`key_version` or record internals).
|
||||||
- **At-use lifecycle** (decision 2) — `vault.decrypt(blob)` →
|
- **At-use lifecycle** (decision 2): `vault.decrypt(blob)` →
|
||||||
**`Zeroizing<String>` wrap at the adapter seam** (decision 3 — the
|
**`Zeroizing<String>` wrap at the adapter seam** (decision 3, the
|
||||||
known-gap contract; the vault-side improvement is upstream, not
|
known-gap contract; the vault-side improvement is upstream, not
|
||||||
here) → one-time `decode_secret_key(plain, None)` → the decoded
|
here) → one-time `decode_secret_key(plain, None)` → the decoded
|
||||||
`PrivateKey` lives for the dial's lifetime. The vault is needed only
|
`PrivateKey` lives for the dial's lifetime. The vault is needed only
|
||||||
at dial time; locking mid-session keeps live sessions streaming.
|
at dial time; locking mid-session keeps live sessions streaming.
|
||||||
- **Host-key load path** (decision 6) — decrypt-at-construction
|
- **Host-key load path** (decision 6): decrypt-at-construction
|
||||||
returning the `PrivateKey` set the server `Config::keys` will
|
returning the `PrivateKey` set the server `Config::keys` will
|
||||||
consume (the `Config` construction itself is wave 2; locked vault →
|
consume (the `Config` construction itself is wave 2; locked vault →
|
||||||
the load fails → **no server is constructed**, POC-2 §5).
|
the load fails → **no server is constructed**, POC-2 §5).
|
||||||
- **Failure mapping** — every vault failure mode maps to its distinct
|
- **Failure mapping**: every vault failure mode maps to its distinct
|
||||||
`KeySourceError` variant (locked / absent / decryption / unknown
|
`KeySourceError` variant (locked / absent / decryption / unknown
|
||||||
version / decode); never collapsed, never a fallback dial.
|
version / decode); never collapsed, never a fallback dial.
|
||||||
- **Plaintext never persists** (decision 7) — no disk spill on this
|
- **Plaintext never persists** (decision 7): no disk spill on this
|
||||||
path, ever; the `Zeroizing` wrap bounds the plaintext from the
|
path, ever; the `Zeroizing` wrap bounds the plaintext from the
|
||||||
adapter seam onward.
|
adapter seam onward.
|
||||||
|
|
||||||
Inner format is OpenSSH private-key PEM, unencrypted (decision 1) —
|
Inner format is OpenSSH private-key PEM, unencrypted (decision 1);
|
||||||
the source contract assumes it; the decode call is the single
|
the source contract assumes it, and the decode call is the single
|
||||||
conversion.
|
conversion.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
@@ -78,31 +78,31 @@ conversion.
|
|||||||
> a *set* because `Config::keys` takes multiple). An assembly may
|
> a *set* because `Config::keys` takes multiple). An assembly may
|
||||||
> implement either trait itself; the crate ships the ADR-005
|
> implement either trait itself; the crate ships the ADR-005
|
||||||
> implementations and the custody machinery.
|
> implementations and the custody machinery.
|
||||||
> - **`VaultKeySource`** — decrypt-at-use over an injected clone of
|
> - **`VaultKeySource`**: decrypt-at-use over an injected clone of
|
||||||
> the assembly's `VaultServiceHandle` + one injected
|
> the assembly's `VaultServiceHandle` + one injected
|
||||||
> `Option<EncryptedData>` record (`None` → `Absent`, the POC-2 §1
|
> `Option<EncryptedData>` record (`None` → `Absent`, the POC-2 §1
|
||||||
> `KeySource { blob: Option<EncryptedData> }` shape).
|
> `KeySource { blob: Option<EncryptedData> }` shape).
|
||||||
> **`VaultHostKeySource`** — same model over a record set (empty set
|
> **`VaultHostKeySource`**: same model over a record set (empty set
|
||||||
> → `Absent`). Both are decrypt-at-use chains
|
> → `Absent`). Both are decrypt-at-use chains
|
||||||
> (`decrypt_and_decode`, shared): `vault.decrypt(blob)` →
|
> (`decrypt_and_decode`, shared): `vault.decrypt(blob)` →
|
||||||
> **`Zeroizing<String>` at the seam** → one-time
|
> **`Zeroizing<String>` at the seam** → one-time
|
||||||
> `decode_secret_key(plain, None)`. Blobs are never interpreted in
|
> `decode_secret_key(plain, None)`. Blobs are never interpreted in
|
||||||
> `src/` — no `key_version` reads, no record internals.
|
> `src/`: no `key_version` reads, no record internals.
|
||||||
> - **Failure mapping** (`map_vault_error`): `VaultLocked` →
|
> - **Failure mapping** (`map_vault_error`): `VaultLocked` →
|
||||||
> `Locked`; `InvalidPath` → `UnknownKeyVersion`; `Encryption` →
|
> `Locked`; `InvalidPath` → `UnknownKeyVersion`; `Encryption` →
|
||||||
> `Decryption`. The vault's decrypt-path-unreachable variants
|
> `Decryption`. The vault's decrypt-path-unreachable variants
|
||||||
> (`Derivation`/`Mnemonic`/`AlreadyUnlocked`/`UnsupportedKeyType`)
|
> (`Derivation`/`Mnemonic`/`AlreadyUnlocked`/`UnsupportedKeyType`)
|
||||||
> land in the `Decryption` refuse-at-crypto class with honest
|
> land in the `Decryption` refuse-at-crypto class with honest
|
||||||
> message text — the five-variant matrix stays closed, nothing
|
> message text; the five-variant matrix stays closed, nothing
|
||||||
> collapses a real custody condition into success.
|
> collapses a real custody condition into success.
|
||||||
> - **Tests** (12, all in-module with the real in-memory
|
> - **Tests** (12, all in-module with the real in-memory
|
||||||
> alkvault handle — `VaultServiceHandle::new()` is the in-memory
|
> alkvault handle: `VaultServiceHandle::new()` is the in-memory
|
||||||
> construction, so no local vault double was needed):
|
> construction, so no local vault double was needed):
|
||||||
> round-trip (encrypt PEM → source → fingerprint-identical decodable
|
> round-trip (encrypt PEM → source → fingerprint-identical decodable
|
||||||
> ed25519 key that signs), dyn-handle load, host-key set order,
|
> ed25519 key that signs), dyn-handle load, host-key set order,
|
||||||
> load-works-after-lock (POC-2 §2 lifecycle), locked / absent /
|
> load-works-after-lock (POC-2 §2 lifecycle), locked / absent /
|
||||||
> tampered-ciphertext / wrong-mnemonic → `Decryption` (distinct
|
> tampered-ciphertext / wrong-mnemonic → `Decryption` (distinct
|
||||||
> from tampered only by message — both GCM-open) / version 0+1 →
|
> from tampered only by message, both GCM-open) / version 0+1 →
|
||||||
> `UnknownKeyVersion` / non-key plaintext → `Decode`, locked
|
> `UnknownKeyVersion` / non-key plaintext → `Decode`, locked
|
||||||
> host-bootstrap refusal, empty host set, v2→v3 rotation-preserves
|
> host-bootstrap refusal, empty host set, v2→v3 rotation-preserves
|
||||||
> -identity through the source. Key fixtures: three committed
|
> -identity through the source. Key fixtures: three committed
|
||||||
@@ -112,12 +112,13 @@ conversion.
|
|||||||
>
|
>
|
||||||
> Interpretation pinned for test-doubles: alkvault 0.1 *does* ship
|
> Interpretation pinned for test-doubles: alkvault 0.1 *does* ship
|
||||||
> the in-memory construction (`VaultServiceHandle`), so the double
|
> the in-memory construction (`VaultServiceHandle`), so the double
|
||||||
> wraps the real handle — no local decrypt/decrypt double.
|
> wraps the real handle; no local decrypt/decrypt double.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
> `src/key_source.rs` (lib re-exports `KeySource`, `HostKeySource`,
|
> `src/key_source.rs` (lib re-exports `KeySource`, `HostKeySource`,
|
||||||
> `VaultKeySource`, `VaultHostKeySource`) — ADR-005 in code: the
|
> `VaultKeySource`, `VaultHostKeySource`), and ADR-005 is in code:
|
||||||
|
> the
|
||||||
> injected-handle source seams both directions consume, the crate's
|
> injected-handle source seams both directions consume, the crate's
|
||||||
> vault-backed implementations carrying decrypt-at-use (vault.decrypt
|
> vault-backed implementations carrying decrypt-at-use (vault.decrypt
|
||||||
> → `Zeroizing<String>` → one-time `decode_secret_key`), opaque
|
> → `Zeroizing<String>` → one-time `decode_secret_key`), opaque
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w1-manifest
|
id: w1-manifest
|
||||||
name: Wave 1 — manifest landing (russh pin + the feature graph)
|
name: "Wave 1: manifest landing (russh pin + the feature graph)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: []
|
depends_on: []
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -11,18 +11,19 @@ level: implementation
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the implementation dependency set and the ADR-003 feature graph —
|
Land the implementation dependency set and the ADR-003 feature graph:
|
||||||
the first commit of wave 1; everything else lands inside it. Per
|
the first commit of wave 1, with everything else landing inside it.
|
||||||
|
Per
|
||||||
ADR-002 decision 6 ("manifest lands with the dependency") and
|
ADR-002 decision 6 ("manifest lands with the dependency") and
|
||||||
`overview.md` §lifecycle note:
|
`overview.md` §lifecycle note:
|
||||||
|
|
||||||
- **`russh = "0.65"`** (caret stops at the minor), `default-features =
|
- **`russh = "0.65"`** (caret stops at the minor), `default-features =
|
||||||
false, features = ["aws-lc-rs"]` — the family crypto posture
|
false, features = ["aws-lc-rs"]` per the family crypto posture
|
||||||
(decision 2), **no `flate2`** (decision 3), **no `rsa`** (decision 4:
|
(decision 2), **no `flate2`** (decision 3), **no `rsa`** (decision 4:
|
||||||
ed25519-only; the tree must drop `rsa`/`pkcs1` RC deps entirely —
|
ed25519-only; the tree must drop `rsa`/`pkcs1` RC deps entirely,
|
||||||
`ssh-key =0.7.0-rc.11` riding the russh keys path is the recorded
|
with `ssh-key =0.7.0-rc.11` riding the russh keys path as the
|
||||||
exception).
|
recorded exception).
|
||||||
- **`alkcall 0.10`** (identity/ACL types at the wire layer —
|
- **`alkcall 0.10`** (identity/ACL types at the wire layer:
|
||||||
`IdentityProvider`, `Identity`, `AccessControl`), **`alkvault 0.1`**
|
`IdentityProvider`, `Identity`, `AccessControl`), **`alkvault 0.1`**
|
||||||
(the ADR-005 custody model). Both pinned by version, never
|
(the ADR-005 custody model). Both pinned by version, never
|
||||||
sibling-checkout paths.
|
sibling-checkout paths.
|
||||||
@@ -31,21 +32,21 @@ ADR-002 decision 6 ("manifest lands with the dependency") and
|
|||||||
- **`edition = "2024"`, `rust-version = "1.89"`** (supersedes the
|
- **`edition = "2024"`, `rust-version = "1.89"`** (supersedes the
|
||||||
current 1.88/2021 manifest).
|
current 1.88/2021 manifest).
|
||||||
- **The ADR-003 feature graph declared from the first commit**:
|
- **The ADR-003 feature graph declared from the first commit**:
|
||||||
`session`, `git`, `tunnel`, `sftp`, `default = []` — flat,
|
`session`, `git`, `tunnel`, `sftp`, `default = []`; the graph is
|
||||||
independent, no feature implies another. Features gate nothing yet
|
flat, independent, and no feature implies another. Features gate
|
||||||
(the wave-2 decline core is their first consumer); the per-feature
|
nothing yet (the wave-2 decline core is their first consumer); the
|
||||||
sibling deps (`alktty` under `session`, `alktunnels` under `tunnel`,
|
per-feature sibling deps (`alktty` under `session`, `alktunnels`
|
||||||
`alkgit` under `git`) land with their first consuming wave, not as
|
under `tunnel`, `alkgit` under `git`) land with their first
|
||||||
dead manifest entries — recorded interpretation, flagged for the
|
consuming wave, not as dead manifest entries (recorded
|
||||||
wave-1 gate.
|
interpretation, flagged for the wave-1 gate).
|
||||||
- Update the manifest comments (the current "deliberately empty until
|
- Update the manifest comments (the current "deliberately empty until
|
||||||
implementation start" notes are superseded); keep comments to the
|
implementation start" notes are superseded); keep comments to the
|
||||||
non-obvious-constraint class only.
|
non-obvious-constraint class only.
|
||||||
|
|
||||||
Pre-flight: verify every pin exists on crates.io at the declared
|
Pre-flight: verify every pin exists on crates.io at the declared
|
||||||
version (`russh 0.65`, `alkcall 0.10`, `alkvault 0.1`); if a
|
version (`russh 0.65`, `alkcall 0.10`, `alkvault 0.1`); if a
|
||||||
crate/version is not published, stop and escalate — a blocking issue,
|
crate/version is not published, stop and escalate, since a missing
|
||||||
not something to work around.
|
pin is a blocking issue, not something to work around.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
@@ -66,24 +67,24 @@ not something to work around.
|
|||||||
- docs/architecture/decisions/002-russh-pin.md (decisions 1–6)
|
- docs/architecture/decisions/002-russh-pin.md (decisions 1–6)
|
||||||
- docs/architecture/decisions/003-feature-graph.md (decision 1, §2)
|
- docs/architecture/decisions/003-feature-graph.md (decision 1, §2)
|
||||||
- docs/architecture/overview.md §lifecycle note, §bounded-resources
|
- docs/architecture/overview.md §lifecycle note, §bounded-resources
|
||||||
- docs/research/russh.md (the 2026-10-26 re-verification section —
|
- docs/research/russh.md (the 2026-10-26 re-verification section:
|
||||||
feature names, tree inventory, MSRV)
|
feature names, tree inventory, MSRV)
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
> Pre-flight executed: `cargo info` confirmed `russh 0.65.0`,
|
> Pre-flight executed: `cargo info` confirmed `russh 0.65.0`,
|
||||||
> `alkcall 0.10.0`, and `alkvault 0.1.0` all published on crates.io —
|
> `alkcall 0.10.0`, and `alkvault 0.1.0` all published on crates.io,
|
||||||
> no blocking issue. `alkcall` pinned as `"0.10"` (latest in-range is
|
> so there is no blocking issue. `alkcall` pinned as `"0.10"` (latest
|
||||||
> 0.10.1; the caret stops at the minor, matching the ADR-002-russh
|
> in-range is 0.10.1; the caret stops at the minor, matching the
|
||||||
> convention for family pins). Toolkit versions: `thiserror 2.0`,
|
> ADR-002-russh convention for family pins). Toolkit versions:
|
||||||
> `tracing 0.1`, `parking_lot 0.12`, `zeroize 1.9`,
|
> `thiserror 2.0`, `tracing 0.1`, `parking_lot 0.12`, `zeroize 1.9`,
|
||||||
> `tokio 1.54` (as already pinned). `alktty`/`alktunnels`/`alkgit`
|
> `tokio 1.54` (as already pinned). `alktty`/`alktunnels`/`alkgit`
|
||||||
> sibling deps deliberately not added — they land with their first
|
> sibling deps deliberately not added, as they land with their first
|
||||||
> consuming wave (recorded interpretation, flagged for the wave-1
|
> consuming wave (recorded interpretation, flagged for the wave-1
|
||||||
> gate as the task states; no dead manifest entries). Manifest
|
> gate as the task states; no dead manifest entries). Manifest
|
||||||
> comments trimmed to the non-obvious-constraint class: the
|
> comments trimmed to the non-obvious-constraint class: the
|
||||||
> russh/`default`-features posture (ADR-002 — must not be re-enabled
|
> russh/`default`-features posture (ADR-002, which must not be
|
||||||
> by a future editor) and the fail-closed feature-graph note (ADR-003).
|
> re-enabled by a future editor) and the fail-closed feature-graph note (ADR-003).
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
@@ -94,8 +95,8 @@ not something to work around.
|
|||||||
> `edition = "2024"`, `rust-version = "1.89"`; ADR-003 feature graph
|
> `edition = "2024"`, `rust-version = "1.89"`; ADR-003 feature graph
|
||||||
> (`session`/`git`/`tunnel`/`sftp`, `default = []`, flat and
|
> (`session`/`git`/`tunnel`/`sftp`, `default = []`, flat and
|
||||||
> independent). Verified: `cargo tree` contains no `rsa`, `pkcs1`, or
|
> independent). Verified: `cargo tree` contains no `rsa`, `pkcs1`, or
|
||||||
> `flate2` — only the recorded `ssh-key 0.7.0-rc.11` exception riding
|
> `flate2` (only the recorded `ssh-key 0.7.0-rc.11` exception riding
|
||||||
> the russh keys path; `cargo test`, `cargo test --all-features`,
|
> the russh keys path); `cargo test`, `cargo test --all-features`,
|
||||||
> `cargo clippy --all-targets -- -D warnings` (default and
|
> `cargo clippy --all-targets -- -D warnings` (default and
|
||||||
> all-features), and `cargo fmt --check` all pass. `taskgraph
|
> all-features), and `cargo fmt --check` all pass. `taskgraph
|
||||||
> validate` clean.
|
> validate` clean.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w1-test-doubles
|
id: w1-test-doubles
|
||||||
name: Wave 1 — test doubles (the in-memory seams)
|
name: "Wave 1: test doubles (the in-memory seams)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w1-error-model, w1-key-sources, w1-host-key-policy]
|
depends_on: [w1-error-model, w1-key-sources, w1-host-key-policy]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -15,21 +15,21 @@ Establish the test-double convention (adapted from the sibling crates)
|
|||||||
and land the in-memory seams every later wave's contract tests run
|
and land the in-memory seams every later wave's contract tests run
|
||||||
against:
|
against:
|
||||||
|
|
||||||
- **In-memory `IdentityProvider`** — registered fingerprints →
|
- **In-memory `IdentityProvider`**: registered fingerprints →
|
||||||
`Identity` resolutions over alkcall's trait; the double the auth
|
`Identity` resolutions over alkcall's trait; the double the auth
|
||||||
bridge and dispatch tests resolve against.
|
bridge and dispatch tests resolve against.
|
||||||
- **In-memory vault seam** — the encrypt/decrypt round-trip plus the
|
- **In-memory vault seam**: the encrypt/decrypt round-trip plus the
|
||||||
failure postures (locked / absent / tampered) behind the same seam
|
failure postures (locked / absent / tampered) behind the same seam
|
||||||
the key sources consume, so the custody tests run without a live
|
the key sources consume, so the custody tests run without a live
|
||||||
vault service. If alkvault 0.1 ships an in-memory construction, wrap
|
vault service. If alkvault 0.1 ships an in-memory construction, wrap
|
||||||
it; otherwise a local test double behind the source trait — the
|
it; otherwise a local test double behind the source trait (the
|
||||||
task pins which, flagged for the wave-1 gate.
|
task pins which, flagged for the wave-1 gate).
|
||||||
- **In-memory `HostKeyPolicy`** — a configurable record/verdict double
|
- **In-memory `HostKeyPolicy`**: a configurable record/verdict double
|
||||||
(accept/reject per host, decision log inspectable) for the dial and
|
(accept/reject per host, decision log inspectable) for the dial and
|
||||||
acceptor tests.
|
acceptor tests.
|
||||||
- **ed25519 test-key fixtures** — throwaway test keys (generated at
|
- **ed25519 test-key fixtures**: throwaway test keys (generated at
|
||||||
test time or committed static PEMs — the task pins which; committed
|
test time or committed static PEMs, with the task pinning which;
|
||||||
keys are throwaway fixtures, not secrets) with recorded
|
committed keys are throwaway fixtures, not secrets) with recorded
|
||||||
`ssh-keygen -lf` fingerprints, shared with the fingerprint parity
|
`ssh-keygen -lf` fingerprints, shared with the fingerprint parity
|
||||||
tests and every later auth/mapping test.
|
tests and every later auth/mapping test.
|
||||||
|
|
||||||
@@ -60,12 +60,12 @@ non-test code.
|
|||||||
- docs/research/poc-2-vault-keys.md (the failure postures the vault
|
- docs/research/poc-2-vault-keys.md (the failure postures the vault
|
||||||
double reproduces)
|
double reproduces)
|
||||||
- The alkgit test-double convention (tasks/implementation/core/
|
- The alkgit test-double convention (tasks/implementation/core/
|
||||||
w1-test-doubles.md — reading reference, never copied code)
|
w1-test-doubles.md; reading reference, never copied code)
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- **Vault-double shape (recorded, flagged for the wave-1 gate):** alkvault
|
- **Vault-double shape (recorded, flagged for the wave-1 gate):** alkvault
|
||||||
0.1 ships an in-memory construction — `VaultServiceHandle::new()` is an
|
0.1 ships an in-memory construction: `VaultServiceHandle::new()` is an
|
||||||
in-process vault that `unlock_new` arms in-memory. The double is a
|
in-process vault that `unlock_new` arms in-memory. The double is a
|
||||||
**wrapper** (`TestVault` in `tests/common/mod.rs`) around that
|
**wrapper** (`TestVault` in `tests/common/mod.rs`) around that
|
||||||
construction, not a local re-impl behind the source trait: the real
|
construction, not a local re-impl behind the source trait: the real
|
||||||
@@ -74,36 +74,39 @@ non-test code.
|
|||||||
`VaultHostKeySource` seams, and the POC-2 §3 postures (locked /
|
`VaultHostKeySource` seams, and the POC-2 §3 postures (locked /
|
||||||
absent / tampered / wrong seed) ride the real seams.
|
absent / tampered / wrong seed) ride the real seams.
|
||||||
- **Fixture decision (recorded):** committed static OpenSSH-PEM test
|
- **Fixture decision (recorded):** committed static OpenSSH-PEM test
|
||||||
keys, minted by `ssh-keygen` at vector-generation time — not generated
|
keys, minted by `ssh-keygen` at vector-generation time, not generated
|
||||||
at test time (determinism, no tooling dependency). Committed private
|
at test time (determinism, no tooling dependency). Committed private
|
||||||
halves are throwaway fixtures by the task's own posture, documented as
|
halves are throwaway fixtures by the task's own posture, documented as
|
||||||
such in `tests/vectors/fingerprints.txt` and in the common module docs.
|
such in `tests/vectors/fingerprints.txt` and in the common module docs.
|
||||||
- **Location decision (recorded):** the doubles live only under
|
- **Location decision (recorded):** the doubles live only under
|
||||||
`tests/common/mod.rs`, reached with `mod common;` — the alkgit
|
`tests/common/mod.rs`, reached with `mod common;` (the alkgit
|
||||||
convention, adapted. Nothing doubles-related is reachable from
|
convention, adapted). Nothing doubles-related is reachable from
|
||||||
`lib.rs`; no feature gates them. `src/`-unit tests that need the
|
`lib.rs`; no feature gates them. `src/`-unit tests that need the
|
||||||
fixture material read the committed *files* via `include_str!`
|
fixture material read the committed *files* via `include_str!`
|
||||||
(`src/key_source.rs` tests), never a copy — the single fixture set is
|
(`src/key_source.rs` tests), never a copy; the single fixture set
|
||||||
|
is
|
||||||
shared by the src-side and tests/-side suites. dead_code is allowed
|
shared by the src-side and tests/-side suites. dead_code is allowed
|
||||||
module-wide in `tests/common/` (each test binary compiles the whole
|
module-wide in `tests/common/` (each test binary compiles the whole
|
||||||
shared module and never exercises all of it).
|
shared module and never exercises all of it).
|
||||||
- **The fixture set was unified and re-minted (flagged for the gate):**
|
- **The fixture set was unified and re-minted (flagged for the gate):**
|
||||||
the previous state had two overlapping throwaway sets — public-only
|
the previous state had two overlapping throwaway sets (public-only
|
||||||
vectors under `tests/vectors/` (fingerprint parity) and inline
|
vectors under `tests/vectors/`, used for fingerprint parity, and
|
||||||
private-PEM constants in `src/key_source.rs` — and the inline PEM
|
inline private-PEM constants in `src/key_source.rs`), and the inline
|
||||||
|
PEM
|
||||||
constants, while decodable by russh, were **rejected by OpenSSH
|
constants, while decodable by russh, were **rejected by OpenSSH
|
||||||
itself** (`ssh-keygen` → "error in libcrypto"): unusable as
|
itself** (`ssh-keygen` → "error in libcrypto"): unusable as
|
||||||
cross-issuer fixtures. All four keys (`alpha`/`bravo`/`charlie`/
|
cross-issuer fixtures. All four keys (`alpha`/`bravo`/`charlie`/
|
||||||
`delta`) were freshly minted with OpenSSH_9.6p1's ssh-keygen, their
|
`delta`) were freshly minted with OpenSSH_9.6p1's ssh-keygen, their
|
||||||
fingerprints recorded byte-verbatim in `tests/vectors/fingerprints.txt`,
|
fingerprints recorded byte-verbatim in `tests/vectors/fingerprints.txt`,
|
||||||
and the two sets unified into one fixture table (`FIXTURE_KEYS`) —
|
and the two sets unified into one fixture table, `FIXTURE_KEYS`
|
||||||
committed `.key` + `.pub` files, served to the fingerprint parity
|
(committed `.key` + `.pub` files, served to the fingerprint parity
|
||||||
tests, the vault round-trip, and every later auth/mapping test. The
|
tests, the vault round-trip, and every later auth/mapping test. The
|
||||||
old vector-only set was retired (its keys had no private halves).
|
old vector-only set was retired (its keys had no private halves).
|
||||||
- The row/stamp discipline is established in `tests/common/mod.rs`'s
|
- The row/stamp discipline is established in `tests/common/mod.rs`'s
|
||||||
module docs (adapted from the sibling crates): rows state properties
|
module docs (adapted from the sibling crates): rows state properties
|
||||||
and carry greppable `Contract stamp:` items citing the pinning
|
and carry greppable `Contract stamp:` items citing the pinning
|
||||||
ADR § — `grep -rn "Contract stamp:" tests/` is the inventory (11
|
ADR §, and `grep -rn "Contract stamp:" tests/` is the inventory
|
||||||
|
(11
|
||||||
stamps across the 11 landed rows).
|
stamps across the 11 landed rows).
|
||||||
- No `src/` non-test changes: `src/key_source.rs` changed only inside
|
- No `src/` non-test changes: `src/key_source.rs` changed only inside
|
||||||
its `#[cfg(test)]` module (fixture `include_str!`s + one parity row);
|
its `#[cfg(test)]` module (fixture `include_str!`s + one parity row);
|
||||||
@@ -113,12 +116,13 @@ non-test code.
|
|||||||
|
|
||||||
The wave-1 test-double convention landed: `tests/common/mod.rs` carries
|
The wave-1 test-double convention landed: `tests/common/mod.rs` carries
|
||||||
`MemIdentityProvider` (registered fingerprints → alkcall `Identity`
|
`MemIdentityProvider` (registered fingerprints → alkcall `Identity`
|
||||||
resolutions; token path resolves none — ADR-004's fingerprint-only
|
resolutions; the token path resolves none (ADR-004's
|
||||||
disposition), `TestVault` (a wrapper over alkvault 0.1's shipped
|
fingerprint-only disposition)), `TestVault` (a wrapper over alkvault 0.1's shipped
|
||||||
in-memory construction — the pinned resolution of the flagged
|
in-memory construction, the pinned resolution of the flagged
|
||||||
vault-double question), `MemHostKeyPolicy` (exact `(host, fingerprint)`
|
vault-double question), `MemHostKeyPolicy` (exact `(host, fingerprint)`
|
||||||
rules + explicit default verdict — fail-closed by default, permissive
|
rules plus an explicit default verdict; fail-closed by default and
|
||||||
only by explicit choice — with a fully inspectable decision log), and
|
permissive only by explicit choice, with a fully inspectable decision
|
||||||
|
log), and
|
||||||
the unified ed25519 fixture table (`FIXTURE_KEYS`: four committed
|
the unified ed25519 fixture table (`FIXTURE_KEYS`: four committed
|
||||||
`ssh-keygen`-minted OpenSSH-PEM key pairs with recorded
|
`ssh-keygen`-minted OpenSSH-PEM key pairs with recorded
|
||||||
`ssh-keygen -lf` fingerprints in `tests/vectors/fingerprints.txt`). The
|
`ssh-keygen -lf` fingerprints in `tests/vectors/fingerprints.txt`). The
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w2-auth-bridge
|
id: w2-auth-bridge
|
||||||
name: Wave 2 — the auth bridge (dispositional handler surface)
|
name: "Wave 2: the auth bridge (dispositional handler surface)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w2-server-mount, w1-fingerprint, w1-test-doubles]
|
depends_on: [w2-server-mount, w1-fingerprint, w1-test-doubles]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -11,32 +11,32 @@ level: implementation
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Fill the Handler skeleton's auth hooks with the ADR-004 dispositions —
|
Fill the Handler skeleton's auth hooks with the ADR-004 dispositions:
|
||||||
the fingerprint → identity bridge, fail-closed on every path:
|
the fingerprint → identity bridge, fail-closed on every path:
|
||||||
|
|
||||||
- **`auth_publickey`** (fires post-verification; the key arrives as
|
- **`auth_publickey`** (fires post-verification; the key arrives as
|
||||||
`VerifiedPublicKey.public_key` with the signature `.algorithm`
|
`VerifiedPublicKey.public_key` with the signature `.algorithm`
|
||||||
exposed): non-ed25519 algorithm → **explicit reject** (ADR-002
|
exposed): non-ed25519 algorithm → **explicit reject** (ADR-002
|
||||||
decision 4 — the explicit-over-inherited pattern, so the posture
|
decision 4, the explicit-over-inherited pattern, so the posture
|
||||||
survives a pin bump); ed25519 → fingerprint via the crate's single
|
survives a pin bump); ed25519 → fingerprint via the crate's single
|
||||||
fingerprint path (`src/fingerprint.rs`, the ADR-004 contract
|
fingerprint path (`src/fingerprint.rs`, the ADR-004 contract
|
||||||
invariant) → `IdentityProvider::resolve_from_fingerprint` →
|
invariant) → `IdentityProvider::resolve_from_fingerprint` →
|
||||||
`Some(identity)` → `Auth::Accept` with the resolved identity stored
|
`Some(identity)` → `Auth::Accept` with the resolved identity stored
|
||||||
in the per-connection handler state (it rides the session for the
|
in the per-connection handler state (it rides the session for the
|
||||||
wave-3 seam propagation — `auth_succeeded` is the ride-along hook);
|
wave-3 seam propagation; `auth_succeeded` is the ride-along hook);
|
||||||
`None` → reject fail-closed. The resolved-identity-`None` path never
|
`None` → reject fail-closed. The resolved-identity-`None` path never
|
||||||
creates a weakly authorized session.
|
creates a weakly authorized session.
|
||||||
- **`auth_publickey_offered`** — accept-and-delegate (the russh
|
- **`auth_publickey_offered`**: accept-and-delegate (the russh
|
||||||
default shape at this pin; keep it explicit so the counting stays
|
default shape at this pin; keep it explicit so the counting stays
|
||||||
honest — the hook does not consume an attempt, POC-1 verified).
|
honest (the hook does not consume an attempt, POC-1 verified).
|
||||||
- **Explicit declines** — `auth_none`, `auth_password`,
|
- **Explicit declines**: `auth_none`, `auth_password`,
|
||||||
`auth_keyboard_interactive`, `auth_openssh_certificate` each get an
|
`auth_keyboard_interactive`, `auth_openssh_certificate` each get an
|
||||||
explicit `Auth::reject()` implementation (family contract, not
|
explicit `Auth::reject()` implementation (family contract, not
|
||||||
inherited defaults — a pin bump could silently change them).
|
inherited defaults, which a pin bump could silently change).
|
||||||
- **The username carries no semantics** (OQ-03) — resolution is
|
- **The username carries no semantics** (OQ-03): resolution is
|
||||||
fingerprint-only; the user string is never an identity input.
|
fingerprint-only; the user string is never an identity input.
|
||||||
|
|
||||||
Auth declines are *dispositions* (`Auth::Reject`), not errors — the
|
Auth declines are *dispositions* (`Auth::Reject`), not errors, so the
|
||||||
error model stays key-source-only. Tests run against the wave-1
|
error model stays key-source-only. Tests run against the wave-1
|
||||||
doubles (`MemIdentityProvider` + the ed25519 fixture keys) at the
|
doubles (`MemIdentityProvider` + the ed25519 fixture keys) at the
|
||||||
handler level; the loopback harness proves the bridge end to end over
|
handler level; the loopback harness proves the bridge end to end over
|
||||||
@@ -65,7 +65,7 @@ the wire in `w2-loopback-harness`.
|
|||||||
dispositions, decisions 1–5)
|
dispositions, decisions 1–5)
|
||||||
- docs/architecture/auth.md §Server direction (the bridge steps; the
|
- docs/architecture/auth.md §Server direction (the bridge steps; the
|
||||||
0.65.0 `VerifiedPublicKey` shape)
|
0.65.0 `VerifiedPublicKey` shape)
|
||||||
- docs/architecture/decisions/002-russh-pin.md (decision 4 —
|
- docs/architecture/decisions/002-russh-pin.md (decision 4;
|
||||||
ed25519-only, the explicit-decline rationale)
|
ed25519-only, the explicit-decline rationale)
|
||||||
- docs/plans/implementation.md §Wave 2 (the auth-bridge bullet)
|
- docs/plans/implementation.md §Wave 2 (the auth-bridge bullet)
|
||||||
- russh 0.65.0 in-source: `Auth`, `VerifiedPublicKey`,
|
- russh 0.65.0 in-source: `Auth`, `VerifiedPublicKey`,
|
||||||
@@ -77,37 +77,37 @@ Implementation record (decisions made at this task's discretion):
|
|||||||
|
|
||||||
- **The bridge core is structured out of the hook on purpose.**
|
- **The bridge core is structured out of the hook on purpose.**
|
||||||
`VerifiedPublicKey` and `PublicKeyOffer` are `#[non_exhaustive]`
|
`VerifiedPublicKey` and `PublicKeyOffer` are `#[non_exhaustive]`
|
||||||
with no external constructor — only russh builds them (and only
|
with no external constructor; only russh builds them (and only
|
||||||
post-verification), so the *full* `auth_publickey`/`auth_publickey_offered`
|
post-verification), so the *full* `auth_publickey`/`auth_publickey_offered`
|
||||||
hooks cannot be invoked directly from a test. The bridge is therefore
|
hooks cannot be invoked directly from a test. The bridge is therefore
|
||||||
`SessionHandler::resolve_bridge(&public_key, &signature_algorithm)` —
|
`SessionHandler::resolve_bridge(&public_key, &signature_algorithm)`,
|
||||||
the ed25519-only gate → the single fingerprint path →
|
spanning the ed25519-only gate → the single fingerprint path →
|
||||||
`resolve_from_fingerprint` — a plain method the contract tests drive
|
`resolve_from_fingerprint`; a plain method the contract tests drive
|
||||||
with ordinary key material, and the hook is one line over it (call,
|
with ordinary key material, and the hook is one line over it (call,
|
||||||
store on `Some`, `Auth::reject()` on `None`). The non-constructible
|
store on `Some`, `Auth::reject()` on `None`). The non-constructible
|
||||||
hooks are exercised as full hooks over the wire instead (see below).
|
hooks are exercised as full hooks over the wire instead (see below).
|
||||||
- **Two test vehicles, both recorded.** Wire rows (the real
|
- **Two test vehicles, both recorded.** Wire rows (the real
|
||||||
`SshAdapter` over a tokio-duplex pair + russh's own client — the
|
`SshAdapter` over a tokio-duplex pair + russh's own client (the
|
||||||
`w2-server-mount` wire-smoke precedent) drive the full
|
`w2-server-mount` wire-smoke precedent)) drive the full
|
||||||
`auth_publickey` hook end to end (accept, fail-closed reject,
|
`auth_publickey` hook end to end (accept, fail-closed reject,
|
||||||
username independence, `auth_none`/`auth_password`/
|
username independence, `auth_none`/`auth_password`/
|
||||||
`auth_openssh_certificate` declines over the wire). Direct-hook rows
|
`auth_openssh_certificate` declines over the wire). Direct-hook rows
|
||||||
against `SessionHandler` (over the wave-1 doubles) drive every
|
against `SessionHandler` (over the wave-1 doubles) drive every
|
||||||
hook with constructible arguments plus `resolve_bridge`. The transport
|
hook with constructible arguments plus `resolve_bridge`. The transport
|
||||||
choice and harness vocabulary remain `w2-loopback-harness`'s to pin —
|
choice and harness vocabulary remain `w2-loopback-harness`'s to
|
||||||
these rows are its thin precursor, not a competing vehicle.
|
pin; these rows are its thin precursor, not a competing vehicle.
|
||||||
- **`SessionHandler` visibility widened as `w2-server-mount`'s note
|
- **`SessionHandler` visibility widened as `w2-server-mount`'s note
|
||||||
anticipated**: the struct is `pub` inside its still-private module,
|
anticipated**: the struct is `pub` inside its still-private module,
|
||||||
re-exported `#[doc(hidden)]` from the crate root — constructible for
|
re-exported `#[doc(hidden)]` from the crate root (constructible for
|
||||||
the contract tests, hidden from the docs; the documented public API
|
the contract tests, hidden from the docs); the documented public API
|
||||||
stays exactly `SshAdapter` + the config/error/host/key-source types.
|
stays exactly `SshAdapter` + the config/error/host/key-source types.
|
||||||
- **Non-ed25519 decline testing at this pin**: no wire row can carry a
|
- **Non-ed25519 decline testing at this pin**: no wire row can carry a
|
||||||
non-ed25519 key — `rsa`/`pkcs1` are dropped from the dependency tree
|
non-ed25519 key: `rsa`/`pkcs1` are dropped from the dependency tree
|
||||||
(ADR-002 decision 4's verification) and ssh-key ships without
|
(ADR-002 decision 4's verification) and ssh-key ships without
|
||||||
ecdsa/dsa/rsa features, so no other key type even parses in-tree.
|
ecdsa/dsa/rsa features, so no other key type even parses in-tree.
|
||||||
The gate is therefore tested on the bridge core with constructible
|
The gate is therefore tested on the bridge core with constructible
|
||||||
stand-in signature algorithms (`SkEd25519`,
|
stand-in signature algorithms (`SkEd25519`,
|
||||||
`SkEcdsaSha2NistP256`, `Other(name@domain)`) — a registered
|
`SkEcdsaSha2NistP256`, `Other(name@domain)`); a registered
|
||||||
fingerprint under a non-ed25519 algorithm still declines, proving
|
fingerprint under a non-ed25519 algorithm still declines, proving
|
||||||
the gate precedes resolution. The gate checks **both** algorithm
|
the gate precedes resolution. The gate checks **both** algorithm
|
||||||
facts (the signature `.algorithm` from the hook and the key's
|
facts (the signature `.algorithm` from the hook and the key's
|
||||||
@@ -116,11 +116,11 @@ Implementation record (decisions made at this task's discretion):
|
|||||||
bump.
|
bump.
|
||||||
- **`auth_publickey_offered`'s accept-and-delegate is explicit but not
|
- **`auth_publickey_offered`'s accept-and-delegate is explicit but not
|
||||||
directly hook-testable at this pin** (non-exhaustive argument; the
|
directly hook-testable at this pin** (non-exhaustive argument; the
|
||||||
russh client sends no `is_real=0` probe — OpenSSH-style clients do,
|
russh client sends no `is_real=0` probe; OpenSSH-style clients do,
|
||||||
and POC-1 + russh's own `publickey_probes_do_not_burn_auth_attempts`
|
and POC-1 + russh's own `publickey_probes_do_not_burn_auth_attempts`
|
||||||
test remain the counting-honesty evidence). The wire rows exercise
|
test remain the counting-honesty evidence). The wire rows exercise
|
||||||
the signed-request path (`is_real=1`) that passes through it.
|
the signed-request path (`is_real=1`) that passes through it.
|
||||||
- **`auth_succeeded` stays unimplemented (the russh default)** — the
|
- **`auth_succeeded` stays unimplemented (the russh default)**: the
|
||||||
identity rides in per-connection handler state
|
identity rides in per-connection handler state
|
||||||
(`SessionHandler::resolved_identity()`, the fail-closed `None`
|
(`SessionHandler::resolved_identity()`, the fail-closed `None`
|
||||||
paths never store); wave 3's seam propagation consumes it there and
|
paths never store); wave 3's seam propagation consumes it there and
|
||||||
@@ -129,7 +129,7 @@ Implementation record (decisions made at this task's discretion):
|
|||||||
wave-3's assertion point.
|
wave-3's assertion point.
|
||||||
- **Certificate fixture added** (`tests/vectors/`): `ca.key`/`ca.pub`
|
- **Certificate fixture added** (`tests/vectors/`): `ca.key`/`ca.pub`
|
||||||
(throwaway CA, committed on the same fixture rationale as every
|
(throwaway CA, committed on the same fixture rationale as every
|
||||||
other private half) and `alpha-cert.pub` — the alpha user
|
other private half) and `alpha-cert.pub`, the alpha user
|
||||||
certificate signed by it (serial 1, key id `alkssh-fixture-alpha`,
|
certificate signed by it (serial 1, key id `alkssh-fixture-alpha`,
|
||||||
principal `git`, validity 2026–2036). Both fingerprints recorded in
|
principal `git`, validity 2026–2036). Both fingerprints recorded in
|
||||||
`fingerprints.txt`. `alpha-cert.pub` drives the
|
`fingerprints.txt`. `alpha-cert.pub` drives the
|
||||||
@@ -139,7 +139,7 @@ Implementation record (decisions made at this task's discretion):
|
|||||||
russh's pre-checks).
|
russh's pre-checks).
|
||||||
- **`MemHostKeySource` double added to `tests/common`** (the wave-1
|
- **`MemHostKeySource` double added to `tests/common`** (the wave-1
|
||||||
doubles' module, still the only doubles location): an
|
doubles' module, still the only doubles location): an
|
||||||
`absent()` shape (load refuses `Absent` — fail-closed if anything
|
`absent()` shape (load refuses `Absent`, fail-closed if anything
|
||||||
ever loads it) and a `from_fixtures()` shape for the wire rows' real
|
ever loads it) and a `from_fixtures()` shape for the wire rows' real
|
||||||
adapter construction (which wants a non-empty host-key set).
|
adapter construction (which wants a non-empty host-key set).
|
||||||
|
|
||||||
@@ -148,22 +148,23 @@ Implementation record (decisions made at this task's discretion):
|
|||||||
The ADR-004 dispositions are landed in `src/handler.rs`:
|
The ADR-004 dispositions are landed in `src/handler.rs`:
|
||||||
`auth_publickey` is the fingerprint → `resolve_from_fingerprint` →
|
`auth_publickey` is the fingerprint → `resolve_from_fingerprint` →
|
||||||
accept-with-identity / reject fail-closed bridge (non-ed25519 keys
|
accept-with-identity / reject fail-closed bridge (non-ed25519 keys
|
||||||
declined *explicitly* by the bridge-core gate before any resolution —
|
declined *explicitly* by the bridge-core gate before any resolution,
|
||||||
ADR-002 decision 4's explicit-over-inherited pattern), with the
|
per ADR-002 decision 4's explicit-over-inherited pattern), with the
|
||||||
resolved identity stored in per-connection handler state
|
resolved identity stored in per-connection handler state
|
||||||
(`SessionHandler::resolved_identity()`, read by wave 3);
|
(`SessionHandler::resolved_identity()`, read by wave 3);
|
||||||
`auth_publickey_offered` is an explicit accept-and-delegate;
|
`auth_publickey_offered` is an explicit accept-and-delegate;
|
||||||
`auth_none`/`auth_password`/`auth_keyboard_interactive`/
|
`auth_none`/`auth_password`/`auth_keyboard_interactive`/
|
||||||
`auth_openssh_certificate` are four explicit family-contract declines;
|
`auth_openssh_certificate` are four explicit family-contract declines;
|
||||||
the username is structurally excluded from resolution (no username
|
the username is structurally excluded from resolution (no username
|
||||||
parameter exists on the bridge core — OQ-03). Every decline is a
|
parameter exists on the bridge core; OQ-03). Every decline is a
|
||||||
`Auth::Reject` disposition, never an error.
|
`Auth::Reject` disposition, never an error.
|
||||||
|
|
||||||
Testing: `tests/auth_bridge.rs` — 7 wire rows over the real adapter
|
Testing: `tests/auth_bridge.rs`, with 7 wire rows over the real
|
||||||
|
adapter
|
||||||
mount (registered-accept, unknown-reject with the `{publickey}`
|
mount (registered-accept, unknown-reject with the `{publickey}`
|
||||||
advertisement surviving, four-username independence, revoke-between-
|
advertisement surviving, four-username independence, revoke-between-
|
||||||
attempts, `auth_none`, `auth_password`, and the OpenSSH-certificate
|
attempts, `auth_none`, `auth_password`, and the OpenSSH-certificate
|
||||||
decline — all via russh's own client over a duplex pair against the
|
decline, all via russh's own client over a duplex pair against the
|
||||||
committed cert fixture) + 7 direct rows against `SessionHandler` and
|
committed cert fixture) + 7 direct rows against `SessionHandler` and
|
||||||
`resolve_bridge` over the wave-1 doubles (all four non-publickey hooks
|
`resolve_bridge` over the wave-1 doubles (all four non-publickey hooks
|
||||||
reject explicitly with no identity state left behind; registered /
|
reject explicitly with no identity state left behind; registered /
|
||||||
@@ -182,5 +183,5 @@ precursor of the `w2-loopback-harness` vehicle (which pins the
|
|||||||
transport choice); flagged for the wave-2 gate: the
|
transport choice); flagged for the wave-2 gate: the
|
||||||
resolved-identity *landing* is code-verifiable only at wave 3 (the
|
resolved-identity *landing* is code-verifiable only at wave 3 (the
|
||||||
hook's russh-owned argument shape is non-exhaustive and the handler
|
hook's russh-owned argument shape is non-exhaustive and the handler
|
||||||
lives inside `run_stream`) — the store rides the same resolved value
|
lives inside `run_stream`), so the store rides the same resolved
|
||||||
the wire rows exercise.
|
value the wire rows exercise.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w2-bounded-resources
|
id: w2-bounded-resources
|
||||||
name: Wave 2 — bounded resources (the in-crate channel cap at the handler surface)
|
name: "Wave 2: bounded resources (the in-crate channel cap at the handler surface)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w2-spawn-return]
|
depends_on: [w2-spawn-return]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -12,11 +12,11 @@ level: implementation
|
|||||||
## Description
|
## Description
|
||||||
|
|
||||||
Enforce the in-crate bounded-resources posture at the handler surface
|
Enforce the in-crate bounded-resources posture at the handler surface
|
||||||
(`overview.md` §Bounded-resources policy; AGENTS.md convention 9 —
|
(`overview.md` §Bounded-resources policy; AGENTS.md convention 9:
|
||||||
SSH servers are internet-facing; unbounded loops and unbounded buffers
|
SSH servers are internet-facing, and unbounded loops and unbounded
|
||||||
are bugs):
|
buffers are bugs):
|
||||||
|
|
||||||
- **The in-crate concurrent-channel cap** — a count of active channels
|
- **The in-crate concurrent-channel cap**, a count of active channels
|
||||||
enforced at every `channel_open_*` hook: opens beyond
|
enforced at every `channel_open_*` hook: opens beyond
|
||||||
`Limits::max_concurrent_channels` are declined fail-closed (a clean
|
`Limits::max_concurrent_channels` are declined fail-closed (a clean
|
||||||
refuse, never queued, never a half-open channel). The count
|
refuse, never queued, never a half-open channel). The count
|
||||||
@@ -25,16 +25,16 @@ are bugs):
|
|||||||
- **The russh knobs are already wired** (`w2-server-mount` mapped
|
- **The russh knobs are already wired** (`w2-server-mount` mapped
|
||||||
`inactivity_timeout` always-`Some`, `max_auth_attempts`,
|
`inactivity_timeout` always-`Some`, `max_auth_attempts`,
|
||||||
`window_size`, `maximum_packet_size`, `channel_buffer_size` from
|
`window_size`, `maximum_packet_size`, `channel_buffer_size` from
|
||||||
`Limits`) — this task verifies them behaviorally where cheap
|
`Limits`); this task verifies them behaviorally where cheap
|
||||||
(e.g., the auth-attempt ceiling disconnects an exceeding client
|
(e.g., the auth-attempt ceiling disconnects an exceeding client
|
||||||
through the harness) and records anything not cheaply observable
|
through the harness) and records anything not cheaply observable
|
||||||
as covered by the config-mapping tests.
|
as covered by the config-mapping tests.
|
||||||
- **Per-length exec-parse limits stay reserved** for the wave-3
|
- **Per-length exec-parse limits stay reserved** for the wave-3
|
||||||
grammar's consumption (`Limits::max_exec_command_len` is carried,
|
grammar's consumption (`Limits::max_exec_command_len` is carried,
|
||||||
not yet enforced — the grammar owns it).
|
not yet enforced; the grammar owns it).
|
||||||
|
|
||||||
The per-identity policy layer (when the alkcall bridge carries it) is
|
The per-identity policy layer (when the alkcall bridge carries it) is
|
||||||
additional and arrives with the bridge itself (wave 3's tunnel leg) —
|
additional and arrives with the bridge itself (wave 3's tunnel leg),
|
||||||
not from this task.
|
not from this task.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
@@ -56,7 +56,7 @@ not from this task.
|
|||||||
table)
|
table)
|
||||||
- docs/architecture/mapping.md §Data-plane / limits
|
- docs/architecture/mapping.md §Data-plane / limits
|
||||||
- docs/plans/implementation.md §Wave 2 (the bounded-resources bullet)
|
- docs/plans/implementation.md §Wave 2 (the bounded-resources bullet)
|
||||||
- src/config.rs (`Limits` — the budget model and its documented
|
- src/config.rs (`Limits`, the budget model and its documented
|
||||||
postures)
|
postures)
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
@@ -65,16 +65,18 @@ not from this task.
|
|||||||
`src/channel_cap.rs` (`ChannelCap`): the per-session active-channel
|
`src/channel_cap.rs` (`ChannelCap`): the per-session active-channel
|
||||||
count with `admit` (check+increment at accept sites) / `has_room`
|
count with `admit` (check+increment at accept sites) / `has_room`
|
||||||
(pure check) / `release` (saturating decrement). The only writer is
|
(pure check) / `release` (saturating decrement). The only writer is
|
||||||
the connection's russh packet loop — every hook runs inline with
|
the connection's russh packet loop, where every hook runs inline
|
||||||
|
with
|
||||||
exclusive `&mut self` (ADR-006 decision 4 puts serving paths on
|
exclusive `&mut self` (ADR-006 decision 4 puts serving paths on
|
||||||
spawned tasks, but their closes cycle back through the packet loop
|
spawned tasks, but their closes cycle back through the packet loop
|
||||||
as `CHANNEL_CLOSE`, firing the hook) — so the `parking_lot`
|
as `CHANNEL_CLOSE`, firing the hook), so the `parking_lot`
|
||||||
convention's short-held *shared* state does not apply: no lock
|
convention's short-held *shared* state does not apply: no lock
|
||||||
exists because no shared mutation does. Saturating `release` means
|
exists because no shared mutation does. Saturating `release` means
|
||||||
a stray decrement cannot manufacture capacity.
|
a stray decrement cannot manufacture capacity.
|
||||||
- **Gate ordering** (handler.rs): feature gate first, then the cap.
|
- **Gate ordering** (handler.rs): feature gate first, then the cap.
|
||||||
Session-channel opens admit-and-count exactly at the accept point.
|
Session-channel opens admit-and-count exactly at the accept point.
|
||||||
The tunnel rows check the cap *before* the seam stub — the stub
|
The tunnel rows check the cap *before* the seam stub, because the
|
||||||
|
stub
|
||||||
declines without opening a channel, so a counted admission it never
|
declines without opening a channel, so a counted admission it never
|
||||||
opens would leak against the cap forever (decline-first-alloc-never
|
opens would leak against the cap forever (decline-first-alloc-never
|
||||||
applies to the budget, not just capability). Wave 3's
|
applies to the budget, not just capability). Wave 3's
|
||||||
@@ -84,25 +86,26 @@ not from this task.
|
|||||||
seam's `ConnectFailed` (pinned by a row, not by convention). x11
|
seam's `ConnectFailed` (pinned by a row, not by convention). x11
|
||||||
declines in every build and never consumes a count.
|
declines in every build and never consumes a count.
|
||||||
- **Decline reason**: `dispatch::cap_exceeded_open_decline()` →
|
- **Decline reason**: `dispatch::cap_exceeded_open_decline()` →
|
||||||
`AdministrativelyProhibited` — the cap is the assembly's configured
|
`AdministrativelyProhibited`: the cap is the assembly's configured
|
||||||
policy ceiling; OpenSSH's own `MaxSessions` answers the same code,
|
policy ceiling; OpenSSH's own `MaxSessions` answers the same code,
|
||||||
so vanilla clients see the vanilla-shaped policy refusal (POC-1 §5).
|
so vanilla clients see the vanilla-shaped policy refusal (POC-1 §5).
|
||||||
- **Decrement invariant**: every fired `channel_close` corresponds to
|
- **Decrement invariant**: every fired `channel_close` corresponds to
|
||||||
exactly one previously-admitted open — russh only routes
|
exactly one previously-admitted open, since russh only routes
|
||||||
`CHANNEL_CLOSE` for channels it considers established (a declined
|
`CHANNEL_CLOSE` for channels it considers established (a declined
|
||||||
open never established one) and consumes the close at most once
|
open never established one) and consumes the close at most once
|
||||||
(the channel is removed from russh's map before the hook).
|
(the channel is removed from russh's map before the hook).
|
||||||
- **Recovery observability (russh-pin fact, recorded in the test
|
- **Recovery observability (russh-pin fact, recorded in the test
|
||||||
doc)**: a *client-initiated* channel close is not observable
|
doc)**: a *client-initiated* channel close is not observable
|
||||||
client-side at this pin — russh removes a locally-closed channel
|
client-side at this pin, because russh removes a locally-closed
|
||||||
|
channel
|
||||||
from its client map before the server's `CHANNEL_CLOSE` arrives
|
from its client map before the server's `CHANNEL_CLOSE` arrives
|
||||||
back, so `Channel::wait()` never delivers the close (the harness's
|
back, so `Channel::wait()` never delivers the close (the harness's
|
||||||
`await_close` observes only *server-initiated* closes, which is what
|
`await_close` observes only *server-initiated* closes, which is what
|
||||||
the spawn-return/dispatch rows use). The recovery row therefore
|
the spawn-return/dispatch rows use). The recovery row therefore
|
||||||
rests on wire ordering (deterministic): close and open ride the
|
rests on wire ordering (deterministic): close and open ride the
|
||||||
client loop's single FIFO event queue, are written in that order,
|
client loop's single FIFO event queue, are written in that order,
|
||||||
and the server's packet loop processes them in order — the counted
|
and the server's packet loop processes them in order, so the
|
||||||
release precedes the recovery admission; had the release not
|
counted release precedes the recovery admission; had the release not
|
||||||
happened, the recovery open declines and the row fails.
|
happened, the recovery open declines and the row fails.
|
||||||
- **`max_auth_attempts` verified behaviorally**: ceiling `2`
|
- **`max_auth_attempts` verified behaviorally**: ceiling `2`
|
||||||
(tunable) → two within-ceiling failures stay ordinary
|
(tunable) → two within-ceiling failures stay ordinary
|
||||||
@@ -110,7 +113,7 @@ not from this task.
|
|||||||
attempt past the ceiling is answered with the server's
|
attempt past the ceiling is answered with the server's
|
||||||
`Disconnect::NoMoreAuthMethodsAvailable`, whose client-side shape is
|
`Disconnect::NoMoreAuthMethodsAvailable`, whose client-side shape is
|
||||||
russh's synthesized `Failure` with an *empty* remaining-methods set
|
russh's synthesized `Failure` with an *empty* remaining-methods set
|
||||||
(no reply came; the advertisement is gone) — the session ends
|
(no reply came; the advertisement is gone), and the session ends
|
||||||
cleanly server-side (row: `tests/bounded_resources.rs`
|
cleanly server-side (row: `tests/bounded_resources.rs`
|
||||||
`auth_attempt_ceiling_disconnects_an_exceeding_client`).
|
`auth_attempt_ceiling_disconnects_an_exceeding_client`).
|
||||||
- **The size/window/clock knobs recorded as config-mapping-covered**:
|
- **The size/window/clock knobs recorded as config-mapping-covered**:
|
||||||
@@ -124,21 +127,21 @@ not from this task.
|
|||||||
the wire buys no new information (russh reads the `Config` fields it
|
the wire buys no new information (russh reads the `Config` fields it
|
||||||
owns); not cheaply behavioral, so recorded, per the task's split.
|
owns); not cheaply behavioral, so recorded, per the task's split.
|
||||||
- **`max_exec_command_len` stays carried, not enforced** (wave 3's
|
- **`max_exec_command_len` stays carried, not enforced** (wave 3's
|
||||||
grammar owns it) — unchanged from `src/config.rs`/`src/dispatch.rs`
|
grammar owns it), unchanged from `src/config.rs`/`src/dispatch.rs`
|
||||||
documentation; nothing here consumes it.
|
documentation; nothing here consumes it.
|
||||||
- **Verification**: `cargo test` (default), and `--features session`
|
- **Verification**: `cargo test` (default), and `--features session`
|
||||||
/ `git` / `tunnel` / `--all-features` (the feature-gated cap rows
|
/ `git` / `tunnel` / `--all-features` (the feature-gated cap rows
|
||||||
run in the builds whose opens can be accepted), `cargo clippy
|
run in the builds whose opens can be accepted), `cargo clippy
|
||||||
--all-targets -- -D warnings` (+ `--all-features`),
|
--all-targets -- -D warnings` (+ `--all-features`),
|
||||||
`cargo fmt --check`, `cargo doc --no-deps` — all green.
|
`cargo fmt --check`, `cargo doc --no-deps`: all green.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Landed the in-crate concurrent-channel cap at the handler surface:
|
Landed the in-crate concurrent-channel cap at the handler surface:
|
||||||
new private `channel_cap` module (count + admit/release semantics,
|
new private `channel_cap` module (count + admit/release semantics,
|
||||||
unit-tested), wired into `SessionHandler`'s every open hook — the
|
unit-tested), wired into `SessionHandler`'s every open hook: the
|
||||||
session accept path admits-and-counts at the accept, the tunnel rows
|
session accept path admits-and-counts at the accept, the tunnel rows
|
||||||
cap-gate before the seam stub, x11 is unaffected — and decremented
|
cap-gate before the seam stub, x11 is unaffected. Counts decrement
|
||||||
through the new `channel_close` hook (single release point, single
|
through the new `channel_close` hook (single release point, single
|
||||||
writer). Cap-exceeded opens decline fail-closed with
|
writer). Cap-exceeded opens decline fail-closed with
|
||||||
`AdministrativelyProhibited` (`dispatch::cap_exceeded_open_decline`,
|
`AdministrativelyProhibited` (`dispatch::cap_exceeded_open_decline`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w2-dispatch-decline-core
|
id: w2-dispatch-decline-core
|
||||||
name: Wave 2 — parse-only dispatch + the fail-closed decline core
|
name: "Wave 2: parse-only dispatch + the fail-closed decline core"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w2-loopback-harness]
|
depends_on: [w2-loopback-harness]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -12,36 +12,40 @@ level: implementation
|
|||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the dispatch skeleton and the fail-closed decline core (ADR-006
|
Land the dispatch skeleton and the fail-closed decline core (ADR-006
|
||||||
as amended, ADR-003) — the routing decisions and the declines; the
|
as amended, ADR-003), which is to say the routing decisions and the
|
||||||
|
declines; the
|
||||||
family-seam serving paths are wave-3 material and arrive as clean
|
family-seam serving paths are wave-3 material and arrive as clean
|
||||||
declining stubs this task shapes the routes for.
|
declining stubs this task shapes the routes for.
|
||||||
|
|
||||||
- **Channel-open gates** (the `channel_open_*` hooks): `session`
|
- **Channel-open gates** (the `channel_open_*` hooks): `session`
|
||||||
channels open only where a session-serving feature exists
|
channels open only where a session-serving feature exists
|
||||||
(`session` ∨ `git` — the parse path lives inside session channels);
|
(`session` ∨ `git`, since the parse path lives inside session
|
||||||
|
channels);
|
||||||
`direct-tcpip` / `forwarded-tcpip` / `direct-streamlocal` gate on
|
`direct-tcpip` / `forwarded-tcpip` / `direct-streamlocal` gate on
|
||||||
the `tunnel` feature; **`x11` and `auth-agent` always decline**
|
the `tunnel` feature; **`x11` and `auth-agent` always decline**
|
||||||
(never feature-gated — ADR-003 §5); every other/unimplemented
|
(never feature-gated, per ADR-003 §5); every other/unimplemented
|
||||||
channel type declines. Unfeatured → decline at open with a clean
|
channel type declines. Unfeatured → decline at open with a clean
|
||||||
wire reason — never a half-open channel.
|
wire reason; never a half-open channel.
|
||||||
- **Parse-only session dispatch** (ADR-006 point 3 as amended):
|
- **Parse-only session dispatch** (ADR-006 point 3 as amended):
|
||||||
`exec_request` parses first — a git-parseable command routes to the
|
`exec_request` parses first: a git-parseable command routes to the
|
||||||
git row, anything else to the session pipe row; `pty_request` /
|
git row, anything else to the session pipe row; `pty_request` /
|
||||||
`shell_request` route to the session rows. **The git-parseability
|
`shell_request` route to the session rows. **The git-parseability
|
||||||
probe here is skeleton-level** (service prefix + path shape); the
|
probe here is skeleton-level** (service prefix + path shape); the
|
||||||
full grammar (quoted/unquoted paths, service-only →
|
full grammar (quoted/unquoted paths, service-only →
|
||||||
decline-with-reason, per-length limits) is wave-3's git task, which
|
decline-with-reason, per-length limits) is wave-3's git task, which
|
||||||
replaces this probe. Parsing happens before dispatch and before any
|
replaces this probe. Parsing happens before dispatch and before any
|
||||||
resource allocation — **decline first, alloc never** (a leaked
|
resource allocation: **decline first, alloc never** (a leaked
|
||||||
channel alloc is a capability leak; the POC-3 stage-A rule).
|
channel alloc is a capability leak; the POC-3 stage-A rule).
|
||||||
- **Request-level declines**: `subsystem_request` — the `sftp` name
|
- **Request-level declines**: `subsystem_request`, where the `sftp`
|
||||||
|
name
|
||||||
declines-with-reason (the OQ-13 placeholder, folded here server
|
declines-with-reason (the OQ-13 placeholder, folded here server
|
||||||
side); git service names decline-with-reason (a subsystem request
|
side); git service names decline-with-reason (a subsystem request
|
||||||
carries only a name, never a repo id); everything else declines.
|
carries only a name, never a repo id); everything else declines.
|
||||||
`env_request` declines unconditionally (OQ-09 — no allowlist ships).
|
`env_request` declines unconditionally (OQ-09; no allowlist ships).
|
||||||
- **Routing stubs**: the git row and the session rows (pty/shell,
|
- **Routing stubs**: the git row and the session rows (pty/shell,
|
||||||
pipe-exec) route to stubs that decline cleanly at the family-seam
|
pipe-exec) route to stubs that decline cleanly at the family-seam
|
||||||
boundary — the seams land in wave 3; the stub shape is documented
|
boundary, since the seams land in wave 3; the stub shape is
|
||||||
|
documented
|
||||||
as skeleton behavior so the routing decisions are contract-tested
|
as skeleton behavior so the routing decisions are contract-tested
|
||||||
now, not after the mappings exist.
|
now, not after the mappings exist.
|
||||||
- **Wire decline reasons are shaped here** (the wave-1 error-model
|
- **Wire decline reasons are shaped here** (the wave-1 error-model
|
||||||
@@ -51,7 +55,7 @@ declining stubs this task shapes the routes for.
|
|||||||
hangs).
|
hangs).
|
||||||
- **Contract tests through the loopback harness, per feature build**:
|
- **Contract tests through the loopback harness, per feature build**:
|
||||||
the skeleton rows of the decline matrix (`mapping.md` §Decline
|
the skeleton rows of the decline matrix (`mapping.md` §Decline
|
||||||
matrix) — none-build declines everything; git-build routes
|
matrix), where none-build declines everything; git-build routes
|
||||||
git-parseable exec (stub at the seam) and declines pty/shell and
|
git-parseable exec (stub at the seam) and declines pty/shell and
|
||||||
plain exec; session-build declines git-parseable exec (the git
|
plain exec; session-build declines git-parseable exec (the git
|
||||||
parse is its gate) and serves its rows only to the stub; tunnel
|
parse is its gate) and serves its rows only to the stub; tunnel
|
||||||
@@ -82,7 +86,7 @@ declining stubs this task shapes the routes for.
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
||||||
(points 2–3; the amendment — parse-only)
|
(points 2–3; the amendment is parse-only)
|
||||||
- docs/architecture/decisions/003-feature-graph.md (the feature graph;
|
- docs/architecture/decisions/003-feature-graph.md (the feature graph;
|
||||||
§5 never-features)
|
§5 never-features)
|
||||||
- docs/architecture/mapping.md §Session-channel dispatch, §Request-level
|
- docs/architecture/mapping.md §Session-channel dispatch, §Request-level
|
||||||
@@ -93,10 +97,10 @@ declining stubs this task shapes the routes for.
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- `src/dispatch.rs` (new): the parse-only dispatch core — the
|
- `src/dispatch.rs` (new): the parse-only dispatch core with the
|
||||||
`ExecRoute` decision (`Git` / `Pipe`), the skeleton exec-parse
|
`ExecRoute` decision (`Git` / `Pipe`), the skeleton exec-parse
|
||||||
probe (`classify_exec`: exact `git-upload-pack`/`git-receive-pack`
|
probe (`classify_exec`: exact `git-upload-pack`/`git-receive-pack`
|
||||||
service prefix + space + path-shaped argument — leading `'` or
|
service prefix + space + path-shaped argument: leading `'` or
|
||||||
`/`; everything else → the pipe row), and the two wire-decline
|
`/`; everything else → the pipe row), and the two wire-decline
|
||||||
reason shapers (`unfeatured_open_decline` →
|
reason shapers (`unfeatured_open_decline` →
|
||||||
`AdministrativelyProhibited`, `tunnel_seam_stub_decline` →
|
`AdministrativelyProhibited`, `tunnel_seam_stub_decline` →
|
||||||
@@ -111,24 +115,24 @@ declining stubs this task shapes the routes for.
|
|||||||
parses first then routes on the feature graph; `pty`/`shell` route
|
parses first then routes on the feature graph; `pty`/`shell` route
|
||||||
under `session`; `subsystem_request` declines every name (sftp =
|
under `session`; `subsystem_request` declines every name (sftp =
|
||||||
the OQ-13 placeholder, `git-*` = no repository id in a name-only
|
the OQ-13 placeholder, `git-*` = no repository id in a name-only
|
||||||
request — ADR-006 decision 2); `env_request` declines
|
request per ADR-006 decision 2); `env_request` declines
|
||||||
unconditionally (OQ-09); `x11_request`/`agent_request` decline
|
unconditionally (OQ-09); `x11_request`/`agent_request` decline
|
||||||
(ADR-003 §5); the four listen requests decline (the flow is wave
|
(ADR-003 §5); the four listen requests decline (the flow is wave
|
||||||
3; never advertise a listen that cannot serve). Decline-first-
|
3; never advertise a listen that cannot serve). Decline-first-
|
||||||
alloc-never holds structurally: no hook allocates before its
|
alloc-never holds structurally: no hook allocates before its
|
||||||
decline; a confirmed session open accepts and drops the channel
|
decline; a confirmed session open accepts and drops the channel
|
||||||
handle (nothing is held this wave — the store-and-take handoff is
|
handle (nothing is held this wave; the store-and-take handoff is
|
||||||
wave-3 mapping material, and the routing decisions around it are
|
wave-3 mapping material, and the routing decisions around it are
|
||||||
already pinned by the seam stubs).
|
already pinned by the seam stubs).
|
||||||
- **Wire decline reasons (the wave-1 error-model reservation, now
|
- **Wire decline reasons (the wave-1 error-model reservation, now
|
||||||
spent):** open-level declines carry channel-open failure codes —
|
spent):** open-level declines carry channel-open failure codes;
|
||||||
unfeatured and never-feature rows answer `AdministrativelyProhibited`
|
unfeatured and never-feature rows answer `AdministrativelyProhibited`
|
||||||
(1) is the honest class: without the feature there is no capability
|
(1) is the honest class: without the feature there is no capability
|
||||||
to distinguish; the tunnel seam stub answers `ConnectFailed` (2),
|
to distinguish; the tunnel seam stub answers `ConnectFailed` (2),
|
||||||
the explicit reject shape ADR-006 decision 1 pins for failed fabric
|
the explicit reject shape ADR-006 decision 1 pins for failed fabric
|
||||||
opens (accept only after the fabric open succeeds; the skeleton
|
opens (accept only after the fabric open succeeds; the skeleton
|
||||||
never opens the fabric). Request-level declines carry
|
never opens the fabric). Request-level declines carry
|
||||||
`channel_failure` — the SSH request vocabulary has no failure text,
|
`channel_failure`: the SSH request vocabulary has no failure text,
|
||||||
so the "reason" is the routing decision, documented per build.
|
so the "reason" is the routing decision, documented per build.
|
||||||
Within request declines, two shapes: a *capability* decline answers
|
Within request declines, two shapes: a *capability* decline answers
|
||||||
failure and leaves the channel usable (POC-1 §5 posture, proven
|
failure and leaves the channel usable (POC-1 §5 posture, proven
|
||||||
@@ -139,11 +143,11 @@ declining stubs this task shapes the routes for.
|
|||||||
global requests auto-reply from the handler's `bool`; every other
|
global requests auto-reply from the handler's `bool`; every other
|
||||||
request hook must call `session.channel_failure` itself. The bare
|
request hook must call `session.channel_failure` itself. The bare
|
||||||
`auth-agent@openssh.com` (and `forwarded-streamlocal@openssh.com`)
|
`auth-agent@openssh.com` (and `forwarded-streamlocal@openssh.com`)
|
||||||
channel *types* have no handler hook — russh rejects those opens
|
channel *types* have no handler hook, so russh rejects those opens
|
||||||
inline (administratively prohibited); that is the decline the
|
inline (administratively prohibited); that is the decline the
|
||||||
never-feature contract needs but it is an inherited posture flagged
|
never-feature contract needs but it is an inherited posture flagged
|
||||||
in the handler docs (pin-bump change would appear as
|
in the handler docs (pin-bump change would appear as
|
||||||
accepted-then-dropped — upstream note). `forwarded-tcpip` has the
|
accepted-then-dropped, an upstream note). `forwarded-tcpip` has the
|
||||||
same gate/stub code shape as its tcp sibling but no client-side
|
same gate/stub code shape as its tcp sibling but no client-side
|
||||||
open helper (the server opens it toward the client), so its wire
|
open helper (the server opens it toward the client), so its wire
|
||||||
rows are wave-3 acceptor material.
|
rows are wave-3 acceptor material.
|
||||||
@@ -170,23 +174,23 @@ declining stubs this task shapes the routes for.
|
|||||||
Landed the parse-only dispatch + the fail-closed decline core
|
Landed the parse-only dispatch + the fail-closed decline core
|
||||||
(`w2-dispatch-decline-core`) in three files plus the harness:
|
(`w2-dispatch-decline-core`) in three files plus the harness:
|
||||||
|
|
||||||
- **`src/dispatch.rs`** — the `ExecRoute` decision, the skeleton
|
- **`src/dispatch.rs`**: the `ExecRoute` decision, the skeleton
|
||||||
exec-parse probe (service prefix + path shape; wave-3 grammar
|
exec-parse probe (service prefix + path shape; wave-3 grammar
|
||||||
replaces), the wire-reason shapers (`AdministrativelyProhibited`
|
replaces), the wire-reason shapers (`AdministrativelyProhibited`
|
||||||
for unfeatured/never-feature opens, `ConnectFailed` for the tunnel
|
for unfeatured/never-feature opens, `ConnectFailed` for the tunnel
|
||||||
seam stub), 7 probe unit rows.
|
seam stub), 7 probe unit rows.
|
||||||
- **`src/handler.rs`** — the `channel_open_*` gates (session ∨ git;
|
- **`src/handler.rs`**: the `channel_open_*` gates (session ∨ git;
|
||||||
tunnel; x11/auth-agent never), the parse-only `exec_request`
|
tunnel; x11/auth-agent never), the parse-only `exec_request`
|
||||||
dispatch, the session-row routes, the request-level declines
|
dispatch, the session-row routes, the request-level declines
|
||||||
(subsystem all-names with the sftp/git named classes, env, x11,
|
(subsystem all-names with the sftp/git named classes, env, x11,
|
||||||
agent), the four listen-request declines, and the named seam
|
agent), the four listen-request declines, and the named seam
|
||||||
stubs (`git_row_stub`, `pipe_row_stub`, `pty_row_stub`,
|
stubs (`git_row_stub`, `pipe_row_stub`, `pty_row_stub`,
|
||||||
`shell_row_stub`, `tunnel_row_stub`) — the wave-3 replacement
|
`shell_row_stub`, `tunnel_row_stub`), which are the wave-3
|
||||||
points, each documented with its family seam. Module docs now
|
replacement points, each documented with its family seam. Module docs now
|
||||||
carry the decline-core contract (gates, decline classes, wire
|
carry the decline-core contract (gates, decline classes, wire
|
||||||
reasons, decline-first-alloc-never, the parse-only posture, the
|
reasons, decline-first-alloc-never, the parse-only posture, the
|
||||||
inherited-auth-agent-open upstream note).
|
inherited-auth-agent-open upstream note).
|
||||||
- **`tests/dispatch_core.rs`** — the skeleton decline-matrix rows per
|
- **`tests/dispatch_core.rs`**: the skeleton decline-matrix rows per
|
||||||
feature build, 26 rows total, each with a `Contract stamp:`:
|
feature build, 26 rows total, each with a `Contract stamp:`:
|
||||||
shared rows in every build (x11 open, `tcpip-forward` decline,
|
shared rows in every build (x11 open, `tcpip-forward` decline,
|
||||||
x11/agent requests where sessions open); none-build
|
x11/agent requests where sessions open); none-build
|
||||||
@@ -197,9 +201,9 @@ Landed the parse-only dispatch + the fail-closed decline core
|
|||||||
declines as gate, stub rows failure+close, capability declines,
|
declines as gate, stub rows failure+close, capability declines,
|
||||||
tunnel unfeatured); tunnel-only (session opens decline, featured
|
tunnel unfeatured); tunnel-only (session opens decline, featured
|
||||||
tunnel rows → `ConnectFailed` stubs); all-features (routes hold
|
tunnel rows → `ConnectFailed` stubs); all-features (routes hold
|
||||||
with the full graph — git row, pipe row, pty/shell stubs,
|
with the full graph: git row, pipe row, pty/shell stubs,
|
||||||
`ConnectFailed` stubs, capability declines).
|
`ConnectFailed` stubs, capability declines).
|
||||||
- **`tests/common/harness.rs`** — the declined-vocabulary helpers
|
- **`tests/common/harness.rs`**: the declined-vocabulary helpers
|
||||||
(tunnel/x11 opens, pty/env/x11/agent requests, listen requests),
|
(tunnel/x11 opens, pty/env/x11/agent requests, listen requests),
|
||||||
module doc updated to record the discharge of the
|
module doc updated to record the discharge of the
|
||||||
`w2-loopback-harness` note.
|
`w2-loopback-harness` note.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w2-loopback-harness
|
id: w2-loopback-harness
|
||||||
name: Wave 2 — the loopback harness (in-process client/server test pair)
|
name: "Wave 2: the loopback harness (in-process client/server test pair)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w2-auth-bridge, w1-test-doubles]
|
depends_on: [w2-auth-bridge, w1-test-doubles]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -12,31 +12,31 @@ level: implementation
|
|||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the in-process client/server test pair every later contract test
|
Land the in-process client/server test pair every later contract test
|
||||||
runs through — the decline matrix, every mapping's contract tests
|
runs through: the decline matrix, every mapping's contract tests
|
||||||
(wave 3), and the client-direction smokes all drive the crate through
|
(wave 3), and the client-direction smokes all drive the crate through
|
||||||
this harness. In-memory doubles from wave 1; real-tool smokes ride the
|
this harness. In-memory doubles from wave 1; real-tool smokes ride the
|
||||||
mapping waves (wave 3+) and the e2e wave (wave 5).
|
mapping waves (wave 3+) and the e2e wave (wave 5).
|
||||||
|
|
||||||
- **Server side** — the `SshAdapter` from `w2-server-mount` over an
|
- **Server side**: the `SshAdapter` from `w2-server-mount` over an
|
||||||
in-process transport. The transport choice (tokio duplex pair vs
|
in-process transport. The transport choice (tokio duplex pair vs
|
||||||
127.0.0.1:0 TCP loopback) is pinned here — both work with
|
127.0.0.1:0 TCP loopback) is pinned here; both work with
|
||||||
`run_stream`/russh's client connect; decide on determinism and
|
`run_stream`/russh's client connect; decide on determinism and
|
||||||
portability and record the choice.
|
portability and record the choice.
|
||||||
- **Client side** — a russh client driven directly (the wave-4 dial
|
- **Client side**: a russh client driven directly (the wave-4 dial
|
||||||
surface does not exist yet; the harness builds the russh client
|
surface does not exist yet; the harness builds the russh client
|
||||||
config from the doubles): in-memory key from the ed25519 fixtures
|
config from the doubles): in-memory key from the ed25519 fixtures
|
||||||
(`FIXTURE_KEYS`), the `MemHostKeyPolicy` double with the permissive
|
(`FIXTURE_KEYS`), the `MemHostKeyPolicy` double with the permissive
|
||||||
test posture (`with_default(true)` — the accept-all posture exists
|
test posture (`with_default(true)`; the accept-all posture exists
|
||||||
only as a test choice, never a crate built-in), the keepalive
|
only as a test choice, never a crate built-in), the keepalive
|
||||||
posture from `Limits`.
|
posture from `Limits`.
|
||||||
- **Harness helpers** — the vocabulary the contract tests speak:
|
- **Harness helpers** (the vocabulary the contract tests speak):
|
||||||
authenticate with a fixture key (and observe auth failure shapes),
|
authenticate with a fixture key (and observe auth failure shapes),
|
||||||
open a channel type (and observe the open-failure reason), send a
|
open a channel type (and observe the open-failure reason), send a
|
||||||
session request (and observe success/failure), await exit-status /
|
session request (and observe success/failure), await exit-status /
|
||||||
close. Helpers stay thin — the russh client API is the vocabulary;
|
close. Helpers stay thin: the russh client API is the vocabulary;
|
||||||
the harness removes boilerplate, not semantics.
|
the harness removes boilerplate, not semantics.
|
||||||
- **Convention** — lives with the wave-1 doubles under `tests/common/`
|
- **Convention**: lives with the wave-1 doubles under `tests/common/`
|
||||||
(or a sibling harness module — same convention, decide and record);
|
(or a sibling harness module, same convention, decide and record);
|
||||||
documented as *the* contract-test vehicle for the crate.
|
documented as *the* contract-test vehicle for the crate.
|
||||||
|
|
||||||
The harness's first consumer is its own acceptance test: a full
|
The harness's first consumer is its own acceptance test: a full
|
||||||
@@ -74,23 +74,23 @@ the wave-1 doubles work live through the wave-2 stack.
|
|||||||
docs):** tokio duplex pair (64 KiB/direction) + russh's
|
docs):** tokio duplex pair (64 KiB/direction) + russh's
|
||||||
`connect_stream`. russh's real codec/kex/transport run over the pair
|
`connect_stream`. russh's real codec/kex/transport run over the pair
|
||||||
verbatim; the one gap versus real sockets (no OS-socket semantics) is
|
verbatim; the one gap versus real sockets (no OS-socket semantics) is
|
||||||
covered more cheaply by the real-tool smokes of wave 3+ — TCP
|
covered more cheaply by the real-tool smokes of wave 3+, since TCP
|
||||||
loopback's listener lifecycle and timing flakiness buy nothing the
|
loopback's listener lifecycle and timing flakiness buy nothing the
|
||||||
harness needs. Deterministic, no port, no collision.
|
harness needs. Deterministic, no port, no collision.
|
||||||
- **Placement decision (recorded, same docs):** `tests/common/harness.rs`,
|
- **Placement decision (recorded, same docs):** `tests/common/harness.rs`,
|
||||||
a sibling module of the wave-1 doubles reached with the same
|
a sibling module of the wave-1 doubles reached with the same
|
||||||
`mod common;` convention — test surface, never a public API export.
|
`mod common;` convention; test surface, never a public API export.
|
||||||
Unlike the doubles it is a *vehicle* convention: later waves call
|
Unlike the doubles it is a *vehicle* convention: later waves call
|
||||||
`common::harness::drive` rather than re-deriving a mount/connect pair.
|
`common::harness::drive` rather than re-deriving a mount/connect pair.
|
||||||
- **russh 0.65 behavior surfaced by the harness (a dispatch-core
|
- **russh 0.65 behavior surfaced by the harness (a dispatch-core
|
||||||
input):** each un-implemented server `channel_open_*` hook drops its
|
input):** each un-implemented server `channel_open_*` hook drops its
|
||||||
`ChannelOpenHandle` → the client observes
|
`ChannelOpenHandle` → the client observes
|
||||||
`ChannelOpenFailure::AdministrativelyProhibited` — the inherited
|
`ChannelOpenFailure::AdministrativelyProhibited`, the inherited
|
||||||
fail-closed decline on the current skeleton stack. The harness's
|
fail-closed decline on the current skeleton stack. The harness's
|
||||||
authenticated-session row matches the reason generically
|
authenticated-session row matches the reason generically
|
||||||
(`Error::ChannelOpenFailure(_)`), so
|
(`Error::ChannelOpenFailure(_)`), so
|
||||||
`w2-dispatch-decline-core`'s reason rows cannot break the vehicle.
|
`w2-dispatch-decline-core`'s reason rows cannot break the vehicle.
|
||||||
- **Scope posture:** the harness is a test-vehicle task — no `src/`
|
- **Scope posture:** the harness is a test-vehicle task, so no `src/`
|
||||||
change. A russh-server behavior note rides the row docs only.
|
change. A russh-server behavior note rides the row docs only.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
@@ -105,22 +105,22 @@ no-port portability) and **sibling-of-the-doubles placement** under
|
|||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
|
|
||||||
- **`HarnessClientHandler`** — the russh client handler bridging the
|
- **`HarnessClientHandler`**: the russh client handler bridging the
|
||||||
`MemHostKeyPolicy` double into `check_server_key`, computing the
|
`MemHostKeyPolicy` double into `check_server_key`, computing the
|
||||||
decision input ([`PresentedHostKey`]) exactly as the wave-4 dial
|
decision input ([`PresentedHostKey`]) exactly as the wave-4 dial
|
||||||
wiring will: endpoints labeled `LOOPBACK_HOST` / `LOOPBACK_PORT`
|
wiring will: endpoints labeled `LOOPBACK_HOST` / `LOOPBACK_PORT`
|
||||||
(the no-port marker `0`), the fingerprint on the crate's single
|
(the no-port marker `0`), the fingerprint on the crate's single
|
||||||
fingerprint path.
|
fingerprint path.
|
||||||
- **`client_config`** — the keepalive/wall-clock posture from `Limits`
|
- **`client_config`**: the keepalive/wall-clock posture from `Limits`
|
||||||
(interval/max verbatim, inactivity always set).
|
(interval/max verbatim, inactivity always set).
|
||||||
- **Server-side assemblers** — `serve_config` / `adapter` from the
|
- **Server-side assemblers**: `serve_config` / `adapter` from the
|
||||||
wave-1 doubles.
|
wave-1 doubles.
|
||||||
- **`drive` → `LoopbackSession`** — the mount-spawn + connect pair,
|
- **`drive` → `LoopbackSession`**: the mount-spawn + connect pair,
|
||||||
with clean-disconnect `unmount`.
|
with clean-disconnect `unmount`.
|
||||||
- **Auth helpers** — `authenticate_publickey` (fixture plumbing
|
- **Auth helpers**: `authenticate_publickey` (fixture plumbing
|
||||||
removed, `AuthResult` verbatim) and `remaining_methods` (the
|
removed, `AuthResult` verbatim) and `remaining_methods` (the
|
||||||
advertisement half of the failure shape).
|
advertisement half of the failure shape).
|
||||||
- **Channel helpers** — `open_session_channel` (open-failure reason
|
- **Channel helpers**: `open_session_channel` (open-failure reason
|
||||||
observed in the `Err`), `request_exec` / `request_shell` /
|
observed in the `Err`), `request_exec` / `request_shell` /
|
||||||
`request_subsystem`, and the message observations: `next_message`,
|
`request_subsystem`, and the message observations: `next_message`,
|
||||||
`await_exit_status`, `await_close`.
|
`await_exit_status`, `await_close`.
|
||||||
@@ -137,10 +137,10 @@ and the `Limits` → client-config posture row.
|
|||||||
Live-exercised doubles: `MemIdentityProvider`, `MemHostKeySource`,
|
Live-exercised doubles: `MemIdentityProvider`, `MemHostKeySource`,
|
||||||
`MemHostKeyPolicy` (both verdicts). `TestVault` stays on the custody
|
`MemHostKeyPolicy` (both verdicts). `TestVault` stays on the custody
|
||||||
rows (fixture keys are unencrypted by contract). The session-request
|
rows (fixture keys are unencrypted by contract). The session-request
|
||||||
and exit-status/close helpers are exercised by construction this wave
|
and exit-status/close helpers are exercised by construction this
|
||||||
— channel opens don't confirm on the decline-only skeleton; their live
|
wave, because channel opens don't confirm on the decline-only
|
||||||
rows arrive with `w2-dispatch-decline-core`.
|
skeleton; their live rows arrive with `w2-dispatch-decline-core`.
|
||||||
|
|
||||||
Verification: `cargo test` (81 passed across 6 binaries),
|
Verification: `cargo test` (81 passed across 6 binaries),
|
||||||
`cargo clippy --all-targets -- -D warnings`, `cargo fmt --check` — all
|
`cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`; all
|
||||||
green.
|
green.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w2-server-mount
|
id: w2-server-mount
|
||||||
name: Wave 2 — SshAdapter::handle mount + russh Config construction
|
name: "Wave 2: SshAdapter::handle mount + russh Config construction"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [review-wave-1, w1-config, w1-key-sources, w1-error-model]
|
depends_on: [review-wave-1, w1-config, w1-key-sources, w1-error-model]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -11,49 +11,51 @@ level: implementation
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the door frame: the `SshAdapter` type — the ADR-001 mount point —
|
Land the door frame: the `SshAdapter` type, the ADR-001 mount point,
|
||||||
and the russh server `Config` construction from the wave-1
|
and the russh server `Config` construction from the wave-1
|
||||||
`ServeConfig`. This is the substrate every mapping composes into; the
|
`ServeConfig`. This is the substrate every mapping composes into; the
|
||||||
auth bridge and dispatch fill the Handler this task skeletons.
|
auth bridge and dispatch fill the Handler this task skeletons.
|
||||||
|
|
||||||
- **`SshAdapter`** — holds the `ServeConfig`; construction
|
- **`SshAdapter`**: holds the `ServeConfig`; construction
|
||||||
(`SshAdapter::new`) loads the host keys decrypt-at-construction via
|
(`SshAdapter::new`) loads the host keys decrypt-at-construction via
|
||||||
the injected `HostKeySource` (ADR-005 decision 6): a locked /
|
the injected `HostKeySource` (ADR-005 decision 6): a locked /
|
||||||
absent / decryption-failed / unknown-version load means **no server
|
absent / decryption-failed / unknown-version load means **no server
|
||||||
is constructed** — the error surfaces as a distinct adapter
|
is constructed**, and the error surfaces as a distinct adapter
|
||||||
construction error wrapping the `KeySourceError` variant (never a
|
construction error wrapping the `KeySourceError` variant (never a
|
||||||
half-configured adapter, never a keyless server that fails later).
|
half-configured adapter, never a keyless server that fails later).
|
||||||
- **russh `Config` construction** (ADR-002 surface notes) — built from
|
- **russh `Config` construction** (ADR-002 surface notes), built from
|
||||||
the serve-config: `methods` = `{publickey}` **exactly**
|
the serve-config: `methods` = `{publickey}` **exactly**
|
||||||
(`MethodSet` from `[MethodKind::Publickey]` — honest advertisement,
|
(`MethodSet` from `[MethodKind::Publickey]`: honest advertisement,
|
||||||
ADR-004 decision 4); `keys` from the loaded host-key set;
|
ADR-004 decision 4); `keys` from the loaded host-key set;
|
||||||
`window_size` / `maximum_packet_size` / `channel_buffer_size` /
|
`window_size` / `maximum_packet_size` / `channel_buffer_size` /
|
||||||
`max_auth_attempts` mapped verbatim from `Limits`;
|
`max_auth_attempts` mapped verbatim from `Limits`;
|
||||||
`inactivity_timeout` mapped **always-`Some`** (the bounded-resources
|
`inactivity_timeout` mapped **always-`Some`** (the bounded-resources
|
||||||
posture — the crate never constructs a session with no wall clock);
|
posture; the crate never constructs a session with no wall clock);
|
||||||
russh's rekey `Limits` stay russh-enforced at their RFC-4253
|
russh's rekey `Limits` stay russh-enforced at their RFC-4253
|
||||||
defaults (recorded in the wave-1 config notes — keep that
|
defaults (recorded in the wave-1 config notes; keep that
|
||||||
recording). `server_id` stays russh's default posture unless the
|
recording). `server_id` stays russh's default posture unless the
|
||||||
task finds a family reason otherwise (decide and record).
|
task finds a family reason otherwise (decide and record).
|
||||||
- **The mount** — `SshAdapter::handle(stream)`: a generic stream in,
|
- **The mount**: `SshAdapter::handle(stream)` with a generic stream
|
||||||
|
in,
|
||||||
the full russh server stack per stream via `russh::server::run_stream`
|
the full russh server stack per stream via `russh::server::run_stream`
|
||||||
(edge termination only — no ALPN code, no relay leg; the `alk/ssh`
|
(edge termination only, with no ALPN code and no relay leg; the
|
||||||
pin is a ledger entry, ADR-001 — nothing here speaks it). The exact
|
`alk/ssh` pin is a ledger entry, ADR-001, and nothing here speaks
|
||||||
|
it). The exact
|
||||||
return shape (awaited `RunningSession`, an internal spawn + join
|
return shape (awaited `RunningSession`, an internal spawn + join
|
||||||
handle, …) is decided here against the spawn-and-return posture
|
handle, …) is decided here against the spawn-and-return posture
|
||||||
(ADR-006 decision 4) and the assembly's mounting needs — decide and
|
(ADR-006 decision 4) and the assembly's mounting needs: decide and
|
||||||
record; it is semver-visible once consumers exist.
|
record; it is semver-visible once consumers exist.
|
||||||
- **The Handler skeleton** — the per-connection `russh::server::Handler`
|
- **The Handler skeleton**: the per-connection `russh::server::Handler`
|
||||||
impl with the adapter's `type Error` (`From<russh::Error>`, thiserror
|
impl with the adapter's `type Error` (`From<russh::Error>` via
|
||||||
— the wave-2-shaped error surface the wave-1 error-model note
|
thiserror, the wave-2-shaped error surface the wave-1 error-model
|
||||||
reserved); every hook left at its fail-closed russh default
|
note reserved); every hook left at its fail-closed russh default
|
||||||
(decline/reject). Later wave-2 tasks fill the auth hooks
|
(decline/reject). Later wave-2 tasks fill the auth hooks
|
||||||
(`w2-auth-bridge`) and the channel/request dispatch
|
(`w2-auth-bridge`) and the channel/request dispatch
|
||||||
(`w2-dispatch-decline-core`).
|
(`w2-dispatch-decline-core`).
|
||||||
|
|
||||||
No mapping seams, no feature-gated serving (wave 3); no client dial
|
No mapping seams, no feature-gated serving (wave 3); no client dial
|
||||||
(wave 4). The feature gates stay untouched — nothing consumes them
|
(wave 4). The feature gates stay untouched, since nothing consumes
|
||||||
yet (the dispatch task is their first `cfg` consumer).
|
them yet (the dispatch task is their first `cfg` consumer).
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
@@ -80,7 +82,7 @@ yet (the dispatch task is their first `cfg` consumer).
|
|||||||
contract: `SshAdapter::handle`, raw vanilla-SSH stream in)
|
contract: `SshAdapter::handle`, raw vanilla-SSH stream in)
|
||||||
- docs/architecture/decisions/002-russh-pin.md (the Config surface
|
- docs/architecture/decisions/002-russh-pin.md (the Config surface
|
||||||
notes; honest advertisement)
|
notes; honest advertisement)
|
||||||
- docs/architecture/decisions/004-auth-dispositions.md (decision 4 —
|
- docs/architecture/decisions/004-auth-dispositions.md (decision 4:
|
||||||
`methods = {publickey}`)
|
`methods = {publickey}`)
|
||||||
- docs/architecture/decisions/005-at-rest-key-custody.md
|
- docs/architecture/decisions/005-at-rest-key-custody.md
|
||||||
(decrypt-at-construction; locked → no server)
|
(decrypt-at-construction; locked → no server)
|
||||||
@@ -94,10 +96,10 @@ yet (the dispatch task is their first `cfg` consumer).
|
|||||||
Implementation record (decisions made at this task's discretion, the
|
Implementation record (decisions made at this task's discretion, the
|
||||||
"decide and record" items):
|
"decide and record" items):
|
||||||
|
|
||||||
- **The mount's return shape — `handle` runs to completion**
|
- **The mount's return shape (`handle` runs to completion)**
|
||||||
(`Result<(), AdapterError>`): `handle` is an async per-stream call
|
(`Result<(), AdapterError>`): `handle` is an async per-stream call
|
||||||
that awaits `run_stream`'s `RunningSession` through the session's
|
that awaits `run_stream`'s `RunningSession` through the session's
|
||||||
end — the alkhttp door precedent (`HttpAdapter`/`ProtocolHandler::
|
end, matching the alkhttp door precedent (`HttpAdapter`/`ProtocolHandler::
|
||||||
handle` is spawned per connection by the assembly's accept loop;
|
handle` is spawned per connection by the assembly's accept loop;
|
||||||
alkhttp/src/server/adapter.rs spawns it in tests the same way).
|
alkhttp/src/server/adapter.rs spawns it in tests the same way).
|
||||||
ADR-006 decision 4 is satisfied inside russh itself: `run_stream`
|
ADR-006 decision 4 is satisfied inside russh itself: `run_stream`
|
||||||
@@ -111,7 +113,7 @@ Implementation record (decisions made at this task's discretion, the
|
|||||||
parameterizes); the second hides the failure boundary behind a
|
parameterizes); the second hides the failure boundary behind a
|
||||||
detached task while still not matching the accept-loop shape any
|
detached task while still not matching the accept-loop shape any
|
||||||
better. The chosen shape's signature is `Result<(), AdapterError>`
|
better. The chosen shape's signature is `Result<(), AdapterError>`
|
||||||
— the smallest honest surface; it is semver-visible and recorded
|
`, the smallest honest surface; it is semver-visible and recorded
|
||||||
here.
|
here.
|
||||||
- **`server_id` stays russh's default** (`SSH-2.0-russh_0.65.0`, the
|
- **`server_id` stays russh's default** (`SSH-2.0-russh_0.65.0`, the
|
||||||
`Config::default()` posture). No family reason found to change it:
|
`Config::default()` posture). No family reason found to change it:
|
||||||
@@ -124,18 +126,19 @@ Implementation record (decisions made at this task's discretion, the
|
|||||||
advertisement; ADR-002 decision 4's "an assembly registers ed25519
|
advertisement; ADR-002 decision 4's "an assembly registers ed25519
|
||||||
fingerprints only" governs what the assembly sources. Flagged for
|
fingerprints only" governs what the assembly sources. Flagged for
|
||||||
the wave-2 gate: if host-key algorithm filtering ever becomes
|
the wave-2 gate: if host-key algorithm filtering ever becomes
|
||||||
wire-contract territory (currently it is not — the key source
|
wire-contract territory (currently it is not, as the key source
|
||||||
contract is silent), it would be an ADR amendment, not this task.
|
contract is silent), it would be an ADR amendment, not this task.
|
||||||
- **Empty key set = `KeySourceError::Absent` even from a custom
|
- **Empty key set = `KeySourceError::Absent` even from a custom
|
||||||
source**: `SshAdapter::new` additionally refuses an `Ok(vec![])`
|
source**: `SshAdapter::new` additionally refuses an `Ok(vec![])`
|
||||||
load as `Construction(Absent)` — `Config::keys` empty would serve
|
load as `Construction(Absent)`, since an empty `Config::keys` would
|
||||||
|
serve
|
||||||
no host key honestly, and the `key_source.rs` wave-1 contract
|
no host key honestly, and the `key_source.rs` wave-1 contract
|
||||||
already documents the empty set as the absent shape.
|
already documents the empty set as the absent shape.
|
||||||
- **Keepalive stays russh-default on the server direction**
|
- **Keepalive stays russh-default on the server direction**
|
||||||
(`None` / 3): the `Limits` keepalive knobs are documented
|
(`None` / 3): the `Limits` keepalive knobs are documented
|
||||||
client-direction posture (reverse-runner evidence); no server-side
|
client-direction posture (reverse-runner evidence); no server-side
|
||||||
keepalive posture exists and none is invented here.
|
keepalive posture exists and none is invented here.
|
||||||
- **`SessionHandler` is `pub(crate)`** — the skeleton's type never
|
- **`SessionHandler` is `pub(crate)`**: the skeleton's type never
|
||||||
appears in a public signature (thanks to the run-to-completion
|
appears in a public signature (thanks to the run-to-completion
|
||||||
return shape), so the public API this task exposes is exactly
|
return shape), so the public API this task exposes is exactly
|
||||||
`SshAdapter` + `AdapterError`; `w2-auth-bridge` can widen visibility
|
`SshAdapter` + `AdapterError`; `w2-auth-bridge` can widen visibility
|
||||||
@@ -166,7 +169,8 @@ stands entirely at russh's fail-closed defaults, with
|
|||||||
`AdapterError` (`Construction(#[from] KeySourceError)` +
|
`AdapterError` (`Construction(#[from] KeySourceError)` +
|
||||||
`Wire(#[from] russh::Error)`) as its `type Error`.
|
`Wire(#[from] russh::Error)`) as its `type Error`.
|
||||||
|
|
||||||
Verification: 12 new tests — advertisement-exactly-publickey, knob
|
Verification: 12 new tests covering advertisement-exactly-publickey,
|
||||||
|
knob
|
||||||
mapping (tuned + documented-default postures), decrypt-at-construction
|
mapping (tuned + documented-default postures), decrypt-at-construction
|
||||||
key-set parity, construction failure matrix (locked / absent / empty /
|
key-set parity, construction failure matrix (locked / absent / empty /
|
||||||
tampered / wrong-vault / unknown-version / decode, variant preserved),
|
tampered / wrong-vault / unknown-version / decode, variant preserved),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w2-spawn-return
|
id: w2-spawn-return
|
||||||
name: Wave 2 — spawn-and-return plumbing + exit/status reporting mechanics
|
name: "Wave 2: spawn-and-return plumbing + exit/status reporting mechanics"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w2-dispatch-decline-core]
|
depends_on: [w2-dispatch-decline-core]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -13,22 +13,23 @@ level: implementation
|
|||||||
|
|
||||||
Land the structural constraint in code (ADR-006 decision 4): russh at
|
Land the structural constraint in code (ADR-006 decision 4): russh at
|
||||||
this pin runs server handler callbacks **inline inside the session's
|
this pin runs server handler callbacks **inline inside the session's
|
||||||
packet loop** — a blocking handler suspends all packet routing for the
|
packet loop**, so a blocking handler suspends all packet routing
|
||||||
connection and deadlocks against the peer's immediate follow-up bytes
|
connection and deadlocks against the peer's immediate follow-up bytes
|
||||||
(POC-3 proved by construction with receive-pack). Therefore:
|
(POC-3 proved by construction with receive-pack). Therefore:
|
||||||
|
|
||||||
- **Every serving path is `tokio::spawn`ed; handlers return fast.**
|
- **Every serving path is `tokio::spawn`ed; handlers return fast.**
|
||||||
The dispatch's routing stubs become spawned serving paths (still
|
The dispatch's routing stubs become spawned serving paths (still
|
||||||
declining stubs — the seams are wave 3), so the pattern is in place
|
declining stubs, since the seams are wave 3), so the pattern is in
|
||||||
|
place
|
||||||
before any real mapping hangs on it.
|
before any real mapping hangs on it.
|
||||||
- **Exit/status reporting mechanics** — reporting from spawned tasks
|
- **Exit/status reporting mechanics**: reporting from spawned tasks
|
||||||
rides the cloned `Session::handle()`: `exit-status` sent **before**
|
rides the cloned `Session::handle()`: `exit-status` sent **before**
|
||||||
channel close, from the spawned task. The *derivation contract's
|
channel close, from the spawned task. The *derivation contract's
|
||||||
rules* (which code → which SSH exit shape) are wave-3
|
rules* (which code → which SSH exit shape) are wave-3
|
||||||
session-mapping material (`mapping.md` rules 1–6); this task lands
|
session-mapping material (`mapping.md` rules 1–6); this task lands
|
||||||
the mechanics — the handle clone, the exit-status send, the
|
the mechanics (the handle clone, the exit-status send, the
|
||||||
ordering-before-close — proven end to end.
|
ordering-before-close), proven end to end.
|
||||||
- **The proof** — a stub serving path that spawns, completes
|
- **The proof**: a stub serving path that spawns, completes
|
||||||
asynchronously (after the handler has returned), and reports
|
asynchronously (after the handler has returned), and reports
|
||||||
`exit-status(N)` + close through the cloned handle; the loopback
|
`exit-status(N)` + close through the cloned handle; the loopback
|
||||||
harness client observes the exit-status arriving after the handler
|
harness client observes the exit-status arriving after the handler
|
||||||
@@ -37,7 +38,7 @@ connection and deadlocks against the peer's immediate follow-up bytes
|
|||||||
constraint as a test, not a convention.
|
constraint as a test, not a convention.
|
||||||
|
|
||||||
This is a load-bearing correctness constraint of the whole spine, not
|
This is a load-bearing correctness constraint of the whole spine, not
|
||||||
an implementation choice — the wave-3 mappings inherit the pattern
|
an implementation choice; the wave-3 mappings inherit the pattern
|
||||||
mechanically from the stubs this task leaves behind.
|
mechanically from the stubs this task leaves behind.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
@@ -57,7 +58,7 @@ mechanically from the stubs this task leaves behind.
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
||||||
(decision 4 — the structural constraint)
|
(decision 4, the structural constraint)
|
||||||
- docs/architecture/mapping.md §The structural constraint:
|
- docs/architecture/mapping.md §The structural constraint:
|
||||||
spawn-and-return; §exit_status row (sent before close, from the
|
spawn-and-return; §exit_status row (sent before close, from the
|
||||||
spawned task)
|
spawned task)
|
||||||
@@ -68,68 +69,69 @@ mechanically from the stubs this task leaves behind.
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- `src/serve.rs` (new): the spawn-and-return plumbing as *mechanics*,
|
- `src/serve.rs` (new): the spawn-and-return plumbing as *mechanics*,
|
||||||
not convention — `spawn_serving_path(serving future)` (the one
|
not convention: `spawn_serving_path(serving future)` (the one
|
||||||
choke point for the ADR-006 decision-4 spawn; nothing it takes is
|
choke point for the ADR-006 decision-4 spawn; nothing it takes is
|
||||||
ever awaited inline) and `report_exit_status_then_close(&Handle,
|
ever awaited inline) and `report_exit_status_then_close(&Handle,
|
||||||
channel, N)` (the cloned-handle report with the `mapping.md`
|
channel, N)` (the cloned-handle report with the `mapping.md`
|
||||||
`exit_status` row's ordering baked in: exit-status **before**
|
`exit_status` row's ordering baked in: exit-status **before**
|
||||||
close). Both re-exported `#[doc(hidden)]` from the crate root — the
|
close). Both are re-exported `#[doc(hidden)]` from the crate root,
|
||||||
`SessionHandler` §Test-surface pattern — as the contract-test
|
the `SessionHandler` §Test-surface pattern, as the contract-test
|
||||||
surface the mechanics rows compose over the real wire. A unit row
|
surface the mechanics rows compose over the real wire. A unit row
|
||||||
(`spawn_returns_while_the_serving_path_is_still_pending` —
|
(`spawn_returns_while_the_serving_path_is_still_pending`,
|
||||||
latch-gated future, spawn call returns first) pins spawn-then-return
|
latch-gated future, spawn call returns first) pins spawn-then-return
|
||||||
directly. The derivation contract's *rules* (which exit code → which
|
directly. The derivation contract's *rules* (which exit code → which
|
||||||
SSH exit shape) stay wave-3 `mapping.md` material; no rule logic
|
SSH exit shape) stay wave-3 `mapping.md` material; no rule logic
|
||||||
landed here.
|
landed here.
|
||||||
- `src/handler.rs`: the four session-row seam stubs (`git_row_stub`,
|
- `src/handler.rs`: the four session-row seam stubs (`git_row_stub`,
|
||||||
`pipe_row_stub`, `pty_row_stub`, `shell_row_stub`) are now spawned
|
`pipe_row_stub`, `pty_row_stub`, `shell_row_stub`) are now spawned
|
||||||
serving paths — `spawn_seam_stub` clones `session.handle()`,
|
serving paths: `spawn_seam_stub` clones `session.handle()`,
|
||||||
`seam_stub_decline(handle, channel)` runs the failure-then-close
|
`seam_stub_decline(handle, channel)` runs the failure-then-close
|
||||||
decline on the spawned task, and the hooks return without awaiting
|
decline on the spawned task, and the hooks return without awaiting
|
||||||
anything. A compile-level fact coded around: the row stubs are
|
anything. A compile-level fact coded around: the row stubs are
|
||||||
plain `fn`s (not `async`) — an `async fn` holding `&Session` across
|
plain `fn`s (not `async`), because an `async fn` holding `&Session`
|
||||||
|
across
|
||||||
the spawn makes the handler future non-`Send` (russh requires `+
|
the spawn makes the handler future non-`Send` (russh requires `+
|
||||||
Send` on handler futures; `Session` is not `Sync`), so the clone is
|
Send` on handler futures; `Session` is not `Sync`), so the clone is
|
||||||
taken synchronously and only the handle moves into the spawned
|
taken synchronously and only the handle moves into the spawned
|
||||||
future. Wire shapes are unchanged (all `w2-dispatch-decline-core`
|
future. Wire shapes are unchanged (all `w2-dispatch-decline-core`
|
||||||
rows pass unmodified with the spawned decline); the tunnel seam stub
|
rows pass unmodified with the spawned decline); the tunnel seam stub
|
||||||
and every open-level/capability decline stay inline by class — they
|
and every open-level/capability decline stay inline by class, as
|
||||||
answer the open request *before* any channel is owned, so nothing
|
they answer the open request *before* any channel is owned, so nothing
|
||||||
spawns yet (documented in the new handler module-doc section §The
|
spawns yet (documented in the new handler module-doc section §The
|
||||||
spawn-and-return structural constraint; wave 3's
|
spawn-and-return structural constraint; wave 3's
|
||||||
accept-only-after-fabric-open flow is where the tunnel row gains
|
accept-only-after-fabric-open flow is where the tunnel row gains
|
||||||
its spawn).
|
its spawn).
|
||||||
- `tests/spawn_return.rs` (new): the end-to-end mechanics proof over
|
- `tests/spawn_return.rs` (new): the end-to-end mechanics proof over
|
||||||
the real wire — the russh server stack (duplex transport, harness
|
the real wire, using the russh server stack (duplex transport,
|
||||||
client/harness transport posture) mounted over a minimal proof
|
harness client/harness transport posture) mounted over a minimal proof
|
||||||
handler that is *not* the crate's `SessionHandler` (the auth and
|
handler that is *not* the crate's `SessionHandler` (the auth and
|
||||||
dispatch hooks have their own suites; the proof subject is the
|
dispatch hooks have their own suites; the proof subject is the
|
||||||
mechanics) and that serves execs by composing the exported
|
mechanics) and that serves execs by composing the exported
|
||||||
plumbing: `spawn_serving_path` of a release-gated serving future
|
plumbing: `spawn_serving_path` of a release-gated serving future
|
||||||
reporting `exit-status(7)` + close through the cloned handle. Two
|
reporting `exit-status(7)` + close through the cloned handle. Two
|
||||||
rows, each with a `Contract stamp:`:
|
rows, each with a `Contract stamp:`:
|
||||||
- `handler_returns_fast_while_the_spawned_serving_path_is_pending` —
|
- `handler_returns_fast_while_the_spawned_serving_path_is_pending`,
|
||||||
the structural constraint as a wire test: after the exec hook has
|
the structural constraint as a wire test: after the exec hook has
|
||||||
provably returned (a bounded wait on the handler-side timestamp;
|
provably returned (a bounded wait on the handler-side timestamp;
|
||||||
the client's exec returns when the request is on the wire, not
|
the client's exec returns when the request is on the wire, not
|
||||||
when the hook ran) and with the serving path still gated, the
|
when the hook ran) and with the serving path still gated, the
|
||||||
client's *follow-up traffic* (a second session channel + second
|
client's *follow-up traffic* (a second session channel + second
|
||||||
exec) is routed and dispatched normally — POC-3's follow-up-bytes
|
exec) is routed and dispatched normally, with POC-3's follow-up-bytes
|
||||||
deadlock shape inverted — and the still-gated no-report state is
|
deadlock shape inverted, and the still-gated no-report state is
|
||||||
asserted before the release.
|
asserted before the release.
|
||||||
- `spawned_stub_reports_exit_status_before_close_after_the_handler_returned`
|
- `spawned_stub_reports_exit_status_before_close_after_the_handler_returned`
|
||||||
— the client observes `ExitStatus{7}` strictly before `Close`
|
with the client observing `ExitStatus{7}` strictly before `Close`
|
||||||
(message-by-message, no draining that could hide ordering), and
|
(message-by-message, no draining that could hide ordering), and
|
||||||
the recorded timestamps prove the exit-status left *after* the
|
the recorded timestamps prove the exit-status left *after* the
|
||||||
handler had already returned (the literal criterion).
|
handler had already returned (the literal criterion).
|
||||||
- **The proof-handler design note:** the gate is `tokio::sync::watch`-
|
- **The proof-handler design note:** the gate is `tokio::sync::watch`-
|
||||||
based (no lost-wakeup race — a late subscriber sees the release,
|
based (no lost-wakeup race; a late subscriber sees the release,
|
||||||
which a `Notify`-with-`notify_waiters` shape could miss against
|
which a `Notify`-with-`notify_waiters` shape could miss against
|
||||||
multiple spawned paths); the handler-side timestamps ride
|
multiple spawned paths); the handler-side timestamps ride
|
||||||
`parking_lot::Mutex`.
|
`parking_lot::Mutex`.
|
||||||
- Feature independence: the mechanics are feature-independent (the
|
- Feature independence: the mechanics are feature-independent (the
|
||||||
helpers exist in every build and the proof rows run in every build
|
helpers exist in every build and the proof rows run in every build
|
||||||
they were verified under — default, `git`, `session`, `tunnel`,
|
they were verified under (default, `git`, `session`, `tunnel`,
|
||||||
`sftp`, `--all-features`); no feature-gated code was added, so no
|
`sftp`, `--all-features`); no feature-gated code was added, so no
|
||||||
new decline-path coverage obligations arise.
|
new decline-path coverage obligations arise.
|
||||||
|
|
||||||
@@ -138,21 +140,22 @@ mechanically from the stubs this task leaves behind.
|
|||||||
Landed the spawn-and-return plumbing + exit/status reporting
|
Landed the spawn-and-return plumbing + exit/status reporting
|
||||||
mechanics (`w2-spawn-return`) in three files:
|
mechanics (`w2-spawn-return`) in three files:
|
||||||
|
|
||||||
- **`src/serve.rs`** — `cargo test`-pinned mechanics: the
|
- **`src/serve.rs`** with `cargo test`-pinned mechanics: the
|
||||||
`spawn_serving_path` choke point (the ADR-006 decision-4 rule, in
|
`spawn_serving_path` choke point (the ADR-006 decision-4 rule, in
|
||||||
code, never awaited inline) and `report_exit_status_then_close`
|
code, never awaited inline) and `report_exit_status_then_close`
|
||||||
(exit-status before close through the cloned `Session::handle()`),
|
(exit-status before close through the cloned `Session::handle()`),
|
||||||
both `#[doc(hidden)]` contract-test re-exports; a unit row pins
|
both `#[doc(hidden)]` contract-test re-exports; a unit row pins
|
||||||
spawn-returns-while-pending directly.
|
spawn-returns-while-pending directly.
|
||||||
- **`src/handler.rs`** — the four session-row seam stubs became
|
- **`src/handler.rs`**: the four session-row seam stubs became
|
||||||
spawned serving paths (handle clone taken synchronously — the
|
spawned serving paths (handle clone taken synchronously, given the
|
||||||
non-`Send` shape that forcing `async fn` stubs would produce is
|
non-`Send` shape that forcing `async fn` stubs would produce is
|
||||||
coded around and recorded); failure-then-close wire shapes unchanged
|
coded around and recorded); failure-then-close wire shapes unchanged
|
||||||
(all dispatch-core rows pass unmodified); open-level declines and
|
(all dispatch-core rows pass unmodified); open-level declines and
|
||||||
the tunnel seam stub stay inline by class, documented; a new
|
the tunnel seam stub stay inline by class, documented; a new
|
||||||
module-doc section carries the constraint and the wave-3
|
module-doc section carries the constraint and the wave-3
|
||||||
inherit-the-pattern note.
|
inherit-the-pattern note.
|
||||||
- **`tests/spawn_return.rs`** — the end-to-end mechanics proof: two
|
- **`tests/spawn_return.rs`**: the end-to-end mechanics proof, with
|
||||||
|
two
|
||||||
`Contract stamp:` rows over the real wire (handler-returns-fast
|
`Contract stamp:` rows over the real wire (handler-returns-fast
|
||||||
under follow-up traffic with the spawn still pending; exit-status
|
under follow-up traffic with the spawn still pending; exit-status
|
||||||
before close, timestamps proving the report outlived the handler's
|
before close, timestamps proving the report outlived the handler's
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w3-decline-tracing
|
id: w3-decline-tracing
|
||||||
name: Wave 3 — decline-path tracing (the wave-2 gate's booked finding)
|
name: "Wave 3: decline-path tracing (the wave-2 gate's booked finding)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w3-seam-config]
|
depends_on: [w3-seam-config]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -13,35 +13,35 @@ level: implementation
|
|||||||
|
|
||||||
Land the wave-2 review gate's booked finding: observability on the
|
Land the wave-2 review gate's booked finding: observability on the
|
||||||
decline paths. `tracing` rides the manifest per the ADR-002 toolkit but
|
decline paths. `tracing` rides the manifest per the ADR-002 toolkit but
|
||||||
so far nothing in `src/` emits — a request-level decline's "reason" is
|
so far nothing in `src/` emits, so a request-level decline's
|
||||||
today only the routing decision documented per build. The real serving
|
"reason" is today only the routing decision documented per build. The real serving
|
||||||
paths land in this wave's mapping tasks, where serve/decline tracing
|
paths land in this wave's mapping tasks, where serve/decline tracing
|
||||||
matters — so the event **vocabulary** lands first and the mappings
|
matters; therefore the event **vocabulary** lands first and the
|
||||||
inherit it.
|
mappings inherit it.
|
||||||
|
|
||||||
- **The family-convention check first** — before choosing the event
|
- **The family-convention check first**: before choosing the event
|
||||||
shape, read the family's established tracing posture (alktty /
|
shape, read the family's established tracing posture (alktty /
|
||||||
alkgit / alktunnels / alkcall `src/`: levels, event/field naming,
|
alkgit / alktunnels / alkcall `src/`: levels, event/field naming,
|
||||||
what gets emitted on error and decline paths) and adopt it — no
|
what gets emitted on error and decline paths) and adopt it (no
|
||||||
alkssh-private dialect; record what was read and chosen.
|
alkssh-private dialect); record what was read and chosen.
|
||||||
- **The event vocabulary** — the deciding context on every decline:
|
- **The event vocabulary**, with the deciding context on every
|
||||||
row + feature build + channel id (+ the decline class — open-level
|
decline being row + feature build + channel id (plus the decline
|
||||||
vs request-level vs serving-path, and where applicable the failure
|
class: open-level vs request-level vs serving-path, and where
|
||||||
shape carried). The mapping tasks then emit at their sites.
|
applicable the failure shape carried). The mapping tasks then emit at their sites.
|
||||||
- **Instrument what exists now** — the open-level declines
|
- **Instrument what exists now**: the open-level declines
|
||||||
(unfeatured / never-feature / cap-exceeded), the request-level
|
(unfeatured / never-feature / cap-exceeded), the request-level
|
||||||
capability declines (`subsystem`/`env`/`x11`/`agent`, the
|
capability declines (`subsystem`/`env`/`x11`/`agent`, the
|
||||||
unfeatured-parse routes), the seam-stub serving declines, and the
|
unfeatured-parse routes), the seam-stub serving declines, and the
|
||||||
serve-path skeleton (spawned path started / ended, failure) — the
|
serve-path skeleton (spawned path started / ended, failure), with
|
||||||
stubs' events carry into the mapping tasks' replaced paths.
|
the stubs' events carrying into the mapping tasks' replaced paths.
|
||||||
- **Safety posture** (conventions 6, OQ-03): no secret material ever
|
- **Safety posture** (conventions 6, OQ-03): no secret material ever
|
||||||
enters an event, never the username; identity/fingerprint exposure
|
enters an event, never the username; identity/fingerprint exposure
|
||||||
on decline events is decided here against the family posture and
|
on decline events is decided here against the family posture and
|
||||||
recorded (auth.md: log exposure is deployment policy — the crate's
|
recorded (auth.md: log exposure is deployment policy, and the
|
||||||
events must be safe-by-default at every level).
|
crate's events must be safe-by-default at every level).
|
||||||
|
|
||||||
Small, cross-cutting unit — one pass over the landed handler/dispatch
|
A single small cross-cutting pass over the landed
|
||||||
sites with the vocabulary, so the three mapping branches never land
|
handler/dispatch sites with the vocabulary, so the three mapping branches never land
|
||||||
inconsistent events.
|
inconsistent events.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
@@ -54,7 +54,7 @@ inconsistent events.
|
|||||||
- [x] The safety posture recorded (no secrets, no username; what
|
- [x] The safety posture recorded (no secrets, no username; what
|
||||||
identity detail may appear decided)
|
identity detail may appear decided)
|
||||||
- [x] A test pins the event presence at the decline sites (or the
|
- [x] A test pins the event presence at the decline sites (or the
|
||||||
doc-test convention the family uses — decide + record)
|
doc-test convention the family uses, decided and recorded)
|
||||||
- [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
- [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||||
`cargo fmt --check`, `cargo doc --no-deps` pass
|
`cargo fmt --check`, `cargo doc --no-deps` pass
|
||||||
|
|
||||||
@@ -66,7 +66,8 @@ inconsistent events.
|
|||||||
classes), §Decline matrix
|
classes), §Decline matrix
|
||||||
- docs/architecture/auth.md §Dispositions (log exposure posture),
|
- docs/architecture/auth.md §Dispositions (log exposure posture),
|
||||||
docs/architecture/decisions/002-russh-pin.md (the toolkit)
|
docs/architecture/decisions/002-russh-pin.md (the toolkit)
|
||||||
- sibling `src/` tracing usage — the convention read this task adopts
|
- sibling `src/` tracing usage, the convention this task reads and
|
||||||
|
adopts
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
@@ -85,8 +86,8 @@ the family's `src/`:
|
|||||||
(`listen = …`, `error = %e`, `reason = …`); levels `debug` for
|
(`listen = …`, `error = %e`, `reason = …`); levels `debug` for
|
||||||
structural observations, `warn` for recoverable failures, `error`
|
structural observations, `warn` for recoverable failures, `error`
|
||||||
for abnormal pump terminations.
|
for abnormal pump terminations.
|
||||||
- **alkcall** (`protocol/*`, `channels/*`): the widest use — the same
|
- **alkcall** (`protocol/*`, `channels/*`): the widest use, with the
|
||||||
fields convention (`channel_id`, `request_id`, `error = %e`),
|
same fields convention (`channel_id`, `request_id`, `error = %e`),
|
||||||
component-prefixed messages (`"channel client: …"`, `"open wrapper:
|
component-prefixed messages (`"channel client: …"`, `"open wrapper:
|
||||||
…"`), `debug`/`warn` only.
|
…"`), `debug`/`warn` only.
|
||||||
- **alkhttp** (the closest structural analog): `warn`/`error` with
|
- **alkhttp** (the closest structural analog): `warn`/`error` with
|
||||||
@@ -100,50 +101,52 @@ free `tracing` fn calls, message-last lowercase with the
|
|||||||
|
|
||||||
### The event vocabulary (landed in `src/observe.rs`)
|
### The event vocabulary (landed in `src/observe.rs`)
|
||||||
|
|
||||||
- **`feature_build`** — the compile-time feature set as a stable
|
- **`feature_build`**, the compile-time feature set as a stable
|
||||||
string (`"none"`, `"git"`, `"session+git"`, …; features join in the
|
string (`"none"`, `"git"`, `"session+git"`, …; features join in the
|
||||||
order `session, git, tunnel, sftp`), so a captured event is
|
order `session, git, tunnel, sftp`), so a captured event is
|
||||||
self-describing per build; `sftp` ships in the string even though it
|
self-describing per build; `sftp` ships in the string even though it
|
||||||
gates no route (it is part of what the assembly deployed).
|
gates no route (it is part of what the assembly deployed).
|
||||||
- **`row`** — the greppable wire row: the channel type for open-level
|
- **`row`**, the greppable wire row: the channel type for open-level
|
||||||
(`"session"`, `"direct-tcpip"`, `"x11"`, …), the request name for
|
(`"session"`, `"direct-tcpip"`, `"x11"`, …), the request name for
|
||||||
request-level (`"pty_request"`, `"tcpip_forward"`, …), the handler
|
request-level (`"pty_request"`, `"tcpip_forward"`, …), the handler
|
||||||
hook name for auth (`"auth_publickey"`, `"auth_none"`, …).
|
hook name for auth (`"auth_publickey"`, `"auth_none"`, …).
|
||||||
- **`decline_class`** — `"open"` | `"request"` | `"serving"` |
|
- **`decline_class`**, `"open"` | `"request"` | `"serving"` |
|
||||||
`"auth"`: the two wire decline classes (`mapping.md` §Wire decline
|
`"auth"`: the two wire decline classes (`mapping.md` §Wire decline
|
||||||
reasons), plus the serving-path class (this wave the seam stub's
|
reasons), plus the serving-path class (this wave the seam stub's
|
||||||
decline *is* the serving path; the mappings emit the same class from
|
decline *is* the serving path; the mappings emit the same class from
|
||||||
their real spawned paths) and the auth class (the ADR-004
|
their real spawned paths) and the auth class (the ADR-004
|
||||||
disposition declines — added by this task: the auth hooks are
|
disposition declines, added by this task: the auth hooks are
|
||||||
decline sites too, and `auth.md`'s dispositions are the crate's
|
decline sites too, and `auth.md`'s dispositions are the crate's
|
||||||
auth-decline contract).
|
auth-decline contract).
|
||||||
- **`reason`** — the decline cause, mirroring the architecture's own
|
- **`reason`**, the decline cause mirroring the architecture's own
|
||||||
classifications: `"unfeatured"`, `"never-feature"` (ADR-003 §5),
|
classifications: `"unfeatured"`, `"never-feature"` (ADR-003 §5),
|
||||||
`"cap-exceeded"`, `"connect-failed"` (tunnel stub / ADR-006
|
`"cap-exceeded"`, `"connect-failed"` (tunnel stub / ADR-006
|
||||||
decision 1), `"seam-stub"` (session-row stubs), `"unimplemented"`
|
decision 1), `"seam-stub"` (session-row stubs), `"unimplemented"`
|
||||||
(subsystem rows — OQ-13 — and the wave-3 listen forms),
|
(the subsystem rows under OQ-13 and the wave-3 listen forms),
|
||||||
`"declined-by-design"` (env — OQ-09), and on the auth class
|
`"declined-by-design"` (env, OQ-09), and on the auth class
|
||||||
`"method-unimplemented"` / `"cert-unsupported"` / `"non-ed25519"` /
|
`"method-unimplemented"` / `"cert-unsupported"` / `"non-ed25519"` /
|
||||||
`"fingerprint-unresolved"`.
|
`"fingerprint-unresolved"`.
|
||||||
- **`channel_id`** — the wire channel id (u32), on every decline of a
|
- **`channel_id`**, the wire channel id (u32), on every decline of a
|
||||||
channel (notably: the event's id is *russh's server-side* id —
|
channel (notably: the event's id is *russh's server-side* id,
|
||||||
`ChannelId` is not publicly constructible, so the helpers take the
|
because `ChannelId` is not publicly constructible, so the helpers take the
|
||||||
raw u32 via `ChannelId::number()`); the auth class and the global
|
raw u32 via `ChannelId::number()`); the auth class and the global
|
||||||
`-R` listen forms carry none (a separate `decline_global_request`
|
`-R` listen forms carry none (a separate `decline_global_request`
|
||||||
helper — global requests have no channel).
|
helper, since global requests have no channel).
|
||||||
- **`exec_route`** — only on `exec_request`-row events: the parse
|
- **`exec_route`**, only on `exec_request`-row events: the parse
|
||||||
route (`"git"` | `"pipe"`), so a git row's decline is
|
route (`"git"` | `"pipe"`), so a git row's decline is
|
||||||
distinguishable from a pipe row's in combined builds. A `None` route
|
distinguishable from a pipe row's in combined builds. A `None` route
|
||||||
field is not recorded at all by `tracing` (discovered by the unit
|
field is not recorded at all by `tracing` (discovered by the unit
|
||||||
rows; recorded in the module docs). The `message` field rides first
|
rows; recorded in the module docs). The `message` field rides first
|
||||||
(the `event!` macro's shape — the unit rows pin the field order).
|
(the `event!` macro's shape, the unit rows pinning the field
|
||||||
|
order).
|
||||||
|
|
||||||
Levels: every decline and the lifecycle's started/ended emit at
|
Levels: every decline and the lifecycle's started/ended emit at
|
||||||
`debug` — a decline is the door's normal, honest posture (vanilla
|
`debug`, since a decline is the door's normal, honest posture
|
||||||
|
(vanilla
|
||||||
clients probe unfeatured rows routinely), matching the family's
|
clients probe unfeatured rows routinely), matching the family's
|
||||||
structural-observation level. Only [`serving_path_failed`] is `warn`
|
structural-observation level. Only [`serving_path_failed`] is `warn`
|
||||||
(a serving path that could not complete its wire reporting is a real
|
(a serving path that could not complete its wire reporting is a real
|
||||||
failure — the family's recoverable-failure level).
|
failure, which is the family's recoverable-failure level).
|
||||||
|
|
||||||
### The serving-path skeleton
|
### The serving-path skeleton
|
||||||
|
|
||||||
@@ -152,40 +155,42 @@ lifecycle events hang off the spawned task's body in
|
|||||||
`handler.rs`'s `seam_stub_decline`: started (the spawned task's first
|
`handler.rs`'s `seam_stub_decline`: started (the spawned task's first
|
||||||
action, so "started" is the task's honest start, not the hook's spawn
|
action, so "started" is the task's honest start, not the hook's spawn
|
||||||
call) → the serving-path decline (class `serving`, reason `seam-stub`)
|
call) → the serving-path decline (class `serving`, reason `seam-stub`)
|
||||||
→ ended — or failed (`warn`, with the russh protocol error) when a
|
→ ended, or failed (`warn`, with the russh protocol error) when a
|
||||||
report send could not complete. The wave-3 mapping branches
|
report send could not complete. The wave-3 mapping branches
|
||||||
(`w3-git-mapping`, `w3-tty-session`, `w3-tunnel-direct`) inherit the
|
(`w3-git-mapping`, `w3-tty-session`, `w3-tunnel-direct`) inherit the
|
||||||
shaped rows by emitting the same helpers from their real serving tasks.
|
shaped rows by emitting the same helpers from their real serving tasks.
|
||||||
|
|
||||||
### The safety posture (conventions 6, OQ-03 — recorded)
|
### The safety posture (conventions 6 and OQ-03, recorded)
|
||||||
|
|
||||||
**No secret material ever enters an event; never the username;
|
**No secret material ever enters an event; never the username;
|
||||||
the fingerprint stays out too.** Structurally enforced, not recalled:
|
the fingerprint stays out too.** Structurally enforced, not recalled:
|
||||||
the vocabulary helpers take only `&'static str` values and the wire
|
the vocabulary helpers take only `&'static str` values and the wire
|
||||||
channel id — no parameter a username, command string, env name/value,
|
channel id, leaving no parameter a username, command string, env
|
||||||
|
name/value,
|
||||||
repo path, key, or fingerprint could ride. The `reason` codes carry
|
repo path, key, or fingerprint could ride. The `reason` codes carry
|
||||||
the decline's *cause class*, and the deciding context is the row +
|
the decline's *cause class*, and the deciding context is the row +
|
||||||
build + channel — deliberately *not* the identity behind the wire
|
build + channel, deliberately *not* the identity behind the wire
|
||||||
attempt. This is the recording of auth.md §Dispositions' standing
|
attempt. This is the recording of auth.md §Dispositions' standing
|
||||||
rule at the crate level: log exposure is deployment policy, so the
|
rule at the crate level: log exposure is deployment policy, so the
|
||||||
crate's events are safe-by-default at every level — the fingerprint
|
crate's events are safe-by-default at every level, with the
|
||||||
|
fingerprint
|
||||||
(and any identity detail) is a deployer-side computation over its own
|
(and any identity detail) is a deployer-side computation over its own
|
||||||
logs, never crate-emitted. If a future decline site needs
|
logs, never crate-emitted. If a future decline site needs
|
||||||
request-argument detail (e.g. the git grammar's decline reasons in
|
request-argument detail (e.g. the git grammar's decline reasons in
|
||||||
`w3-git-mapping`), it must add an explicitly-vocabulary'd field and
|
`w3-git-mapping`), it must add an explicitly-vocabulary'd field and
|
||||||
record its safety reasoning — dynamic strings do not ride
|
record its safety reasoning, since dynamic strings do not ride
|
||||||
`src/observe.rs` by default. The wire rows pin the posture by
|
`src/observe.rs` by default. The wire rows pin the posture by
|
||||||
scanning every captured event: no username string, no `SHA256:`
|
scanning every captured event: no username string, no `SHA256:`
|
||||||
fingerprint, no `user*`-named field ever appears.
|
fingerprint, no `user*`-named field ever appears.
|
||||||
|
|
||||||
### The test convention (decided + recorded)
|
### The test convention (decided + recorded)
|
||||||
|
|
||||||
The family has no event-capture precedent to adopt — alktunnels'
|
The family has no event-capture precedent to adopt: alktunnels'
|
||||||
`tracing-subscriber` dev-dep drives fmt output, not assertions. The
|
`tracing-subscriber` dev-dep drives fmt output, not assertions. The
|
||||||
convention these rows pin: **a hand-rolled minimal
|
convention these rows pin: **a hand-rolled minimal
|
||||||
`tracing::Subscriber`** capturing events into a shared buffer,
|
`tracing::Subscriber`** capturing events into a shared buffer,
|
||||||
installed once as the global default (`tests/observe.rs`) — the
|
installed once as the global default (`tests/observe.rs`), because
|
||||||
server side runs on spawned tasks, so a thread-local
|
the server side runs on spawned tasks, so a thread-local
|
||||||
`with_default` capture cannot see them, and a global capture needs no
|
`with_default` capture cannot see them, and a global capture needs no
|
||||||
new dependency. Two levels of pinning:
|
new dependency. Two levels of pinning:
|
||||||
|
|
||||||
@@ -195,16 +200,17 @@ new dependency. Two levels of pinning:
|
|||||||
lifecycle messages, the `warn` failure arm, and the
|
lifecycle messages, the `warn` failure arm, and the
|
||||||
cfg-derived `feature_build` string.
|
cfg-derived `feature_build` string.
|
||||||
- Wire-level (`tests/observe.rs`, the loopback harness drives the
|
- Wire-level (`tests/observe.rs`, the loopback harness drives the
|
||||||
real wire; capture installed up front via the mount helpers — a
|
real wire; capture installed up front via the mount helpers, since
|
||||||
lazily-installed global loses the earliest emissions): presence by
|
a lazily-installed global loses the earliest emissions): presence by
|
||||||
field-matching at every decline site across the builds — x11 open,
|
field-matching at every decline site across the builds: x11 open,
|
||||||
`-R` listen (global), the fingerprint-unresolved and auth_none
|
`-R` listen (global), the fingerprint-unresolved and auth_none
|
||||||
dispositions (driven on fresh mounts — a second auth exchange on an
|
dispositions (driven on fresh mounts, since a second auth exchange
|
||||||
|
on an
|
||||||
already-authenticated session is not a supported russh surface, as
|
already-authenticated session is not a supported russh surface, as
|
||||||
the probe discovered), the unfeatured session/pipe/pty rows, the
|
the probe discovered), the unfeatured session/pipe/pty rows, the
|
||||||
git seam-stub's full lifecycle, the tunnel stub's `connect-failed`,
|
git seam-stub's full lifecycle, the tunnel stub's `connect-failed`,
|
||||||
the cap-exceeded row (zero-cap limits must ride the *server* config
|
the cap-exceeded row (zero-cap limits must ride the *server* config
|
||||||
— `serve_config`, not just the client's `Limits`), and the
|
(`serve_config`), not just the client's `Limits`), and the
|
||||||
username/fingerprint safety scan. Build-specific rows follow the
|
username/fingerprint safety scan. Build-specific rows follow the
|
||||||
`dispatch_core.rs` per-build cfg convention and were verified in
|
`dispatch_core.rs` per-build cfg convention and were verified in
|
||||||
the default, `git`, `session`, `tunnel`, and `--all-features`
|
the default, `git`, `session`, `tunnel`, and `--all-features`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w3-git-mapping
|
id: w3-git-mapping
|
||||||
name: Wave 3 — the git mapping (exec grammar + alkgit dispatch)
|
name: "Wave 3: the git mapping (exec grammar + alkgit dispatch)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w3-decline-tracing]
|
depends_on: [w3-decline-tracing]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -11,28 +11,29 @@ level: implementation
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the git branch of the spine (ADR-006 decision 2, exec-only) — the
|
Land the git branch of the spine (ADR-006 decision 2, exec-only),
|
||||||
alkforge primary purpose: the full exec grammar replaces the wave-2
|
which is the alkforge primary purpose: the full exec grammar replaces the wave-2
|
||||||
skeleton probe, and the git row becomes a real dispatch to the
|
skeleton probe, and the git row becomes a real dispatch to the
|
||||||
published alkgit seam. Everything POC-3 stage A proved, re-landed as
|
published alkgit seam. Everything POC-3 stage A proved, re-landed as
|
||||||
contract.
|
contract.
|
||||||
|
|
||||||
- **The full grammar** (replaces `dispatch::classify_exec` — the
|
- **The full grammar** (replaces `dispatch::classify_exec`, the
|
||||||
wave-2 interpretation's named replacement; its 7 probe unit rows
|
wave-2 interpretation's named replacement; its 7 probe unit rows
|
||||||
migrate): `git-{upload,receive}-pack '<path>'` — quoted/unquoted
|
migrate): `git-{upload,receive}-pack '<path>'` handles quoted/unquoted
|
||||||
paths; strip the quotes and the leading `/` (POC-3 stage A rows:
|
paths; strip the quotes and the leading `/` (POC-3 stage A rows:
|
||||||
what real OpenSSH git sends); unknown services
|
what real OpenSSH git sends); unknown services
|
||||||
(`git-send-pack`, `git-upload-archive`) → decline-with-reason, the
|
(`git-send-pack`, `git-upload-archive`) → decline-with-reason, the
|
||||||
honest surface; service-only (no repo) → decline-with-reason —
|
honest surface; service-only (no repo) → decline-with-reason (POC-3
|
||||||
POC-3 §A: SSH exec must carry the repo; per-length limits consumed
|
§A: SSH exec must carry the repo); per-length limits consumed
|
||||||
at last — `Limits::max_exec_command_len` enforced **before** any
|
at last, with `Limits::max_exec_command_len` enforced **before**
|
||||||
|
any
|
||||||
parse or allocation (decline-first, alloc-never; the wave-2
|
parse or allocation (decline-first, alloc-never; the wave-2
|
||||||
carried-not-enforced reservation). Decide the wire shapes per
|
carried-not-enforced reservation). Decide the wire shapes per
|
||||||
decline class (open/reply vocabulary, whether "unknown service" and
|
decline class (open/reply vocabulary, whether "unknown service" and
|
||||||
"not git at all" share a reason shape) and record.
|
"not git at all" share a reason shape) and record.
|
||||||
- **Repo path → repo_id, no existence oracle**: the parse validates
|
- **Repo path → repo_id, no existence oracle**: the parse validates
|
||||||
via `alkgit::validate_repo_id` (1–2 segments, charset, ≤ 100 bytes)
|
via `alkgit::validate_repo_id` (1–2 segments, charset, ≤ 100 bytes)
|
||||||
and hands the id through — alkssh never resolves; the
|
and hands the id through, alkssh never resolving; the
|
||||||
**collapsed generic denial before any protocol byte** is
|
**collapsed generic denial before any protocol byte** is
|
||||||
`serve_duplex_resolved`'s `SubstrateError::AccessDenied` posture,
|
`serve_duplex_resolved`'s `SubstrateError::AccessDenied` posture,
|
||||||
contract-pinned here: over the SSH channel, no pack/ref byte
|
contract-pinned here: over the SSH channel, no pack/ref byte
|
||||||
@@ -40,30 +41,30 @@ contract.
|
|||||||
test row). Decide the `.git`-suffix normalization against the
|
test row). Decide the `.git`-suffix normalization against the
|
||||||
alkgit registry contract (what vanilla git sends vs
|
alkgit registry contract (what vanilla git sends vs
|
||||||
`validate_repo_id`'s grammar) and record.
|
`validate_repo_id`'s grammar) and record.
|
||||||
- **The dispatch** — the git row becomes real: the taken channel
|
- **The dispatch**: the git row becomes real, and the taken channel
|
||||||
(the store-and-take handoff lands **here first** — the accepted
|
(the store-and-take handoff lands **here first**; the accepted
|
||||||
channel is stored at the open, `channels.remove(&id)` →
|
channel is stored at the open, `channels.remove(&id)` →
|
||||||
`into_stream()` in the spawned serving task; the session task
|
`into_stream()` in the spawned serving task; the session task
|
||||||
reuses the store) → `alkgit::serve_duplex_resolved(repo_id,
|
reuses the store) → `alkgit::serve_duplex_resolved(repo_id,
|
||||||
identity, service, duplex, limits, registry, refs, packgen,
|
identity, service, duplex, limits, registry, refs, packgen,
|
||||||
ingest, push_options_negotiated, pipeline)` — the serve-config's
|
ingest, push_options_negotiated, pipeline)`, namely the
|
||||||
`git` seam handle (`w3-seam-config`), the resolved identity riding
|
serve-config's `git` seam handle (`w3-seam-config`), the resolved identity riding
|
||||||
the `identity` param. Process-mode dialect: alkgit speaks first
|
the `identity` param. Process-mode dialect: alkgit speaks first
|
||||||
(the V0 advertisement, no service prefix) — nothing alkssh-side;
|
(the V0 advertisement, no service prefix), and nothing is
|
||||||
verify the dialect holds over the channel duplex with a byte-level
|
alkssh-side. Verify the dialect holds over the channel duplex with a byte-level
|
||||||
row.
|
row.
|
||||||
- **Exit-status both directions** (POC-3 §A.5): clean serve end →
|
- **Exit-status both directions** (POC-3 §A.5): clean serve end →
|
||||||
`exit-status(0)`... decide the code mapping (`SubstrateError`
|
`exit-status(0)`... decide the code mapping (`SubstrateError`
|
||||||
outcomes → the code — POC evidence: denied → the client observed
|
outcomes → the code; POC evidence: denied → the client observed
|
||||||
`ExitStatus{1}`; vanilla git exits 0 on success) — sent before
|
`ExitStatus{1}`; vanilla git exits 0 on success), sent before
|
||||||
close, from the spawned task via the cloned `Session::handle()`
|
close, from the spawned task via the cloned `Session::handle()`
|
||||||
(the wave-2 mechanics reused). Record the decision.
|
(the wave-2 mechanics reused). Record the decision.
|
||||||
- **Identity** — the parked wave-2 deferred item discharges here:
|
- **Identity**: the parked wave-2 deferred item discharges here, and
|
||||||
the resolved identity (the bridge's `auth_succeeded` ride-along)
|
the resolved identity (the bridge's `auth_succeeded` ride-along)
|
||||||
reaches the seam — assert it live (the authorize-path/witness row
|
reaches the seam, so assert it live (the authorize-path/witness row
|
||||||
the harness can observe; decide the cheapest row) so the
|
the harness can observe; decide the cheapest row) so the
|
||||||
visible-surface invariant is observable through the git row.
|
visible-surface invariant is observable through the git row.
|
||||||
- **Contract rows through the harness, per build** — the
|
- **Contract rows through the harness, per build**, with the
|
||||||
`tests/dispatch_core.rs` skeleton rows upgraded: git-only serves
|
`tests/dispatch_core.rs` skeleton rows upgraded: git-only serves
|
||||||
the git row through the doubles (clone/push semantics over the
|
the git row through the doubles (clone/push semantics over the
|
||||||
in-mem composition), declines plain exec/pty/shell (parse-then-fail,
|
in-mem composition), declines plain exec/pty/shell (parse-then-fail,
|
||||||
@@ -72,12 +73,12 @@ contract.
|
|||||||
routing holds. The declined rows keep their wire shapes
|
routing holds. The declined rows keep their wire shapes
|
||||||
(`channel_failure`, channel usable / serving-path failure+close per
|
(`channel_failure`, channel usable / serving-path failure+close per
|
||||||
class).
|
class).
|
||||||
- **The real-tool smoke** — one `git clone` (2.43+) through the
|
- **The real-tool smoke**: one `git clone` (2.43+) through the
|
||||||
harness against hand-rolled in-mem alkgit doubles (alkgit's `Mem*`
|
harness against hand-rolled in-mem alkgit doubles (alkgit's `Mem*`
|
||||||
are test-private — alkssh hand-rolls the minimal quartet per
|
are test-private, so alkssh hand-rolls the minimal quartet per
|
||||||
`alkgit/src/backend.rs:164-254`'s noop shapes; never sibling-checkout
|
`alkgit/src/backend.rs:164-254`'s noop shapes; never sibling-checkout
|
||||||
paths, never copies of the doubles' code). Decide the tooling
|
paths, never copies of the doubles' code). Decide the tooling
|
||||||
posture (require git in the environment vs conditional-skip —
|
posture (require git in the environment vs conditional-skip,
|
||||||
skipped-with-reason at most, never silently green) and record.
|
skipped-with-reason at most, never silently green) and record.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
@@ -106,10 +107,10 @@ contract.
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- docs/architecture/mapping.md (the git channel-type row; §Session-
|
- docs/architecture/mapping.md (the git channel-type row; §Session-
|
||||||
channel dispatch — decline-first-alloc-never; §the structural
|
channel dispatch and its decline-first-alloc-never rule; §the
|
||||||
constraint)
|
structural constraint)
|
||||||
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
||||||
(decision 2 — exec-only; decision 4 — spawn-and-return)
|
(decision 2, exec-only; decision 4, spawn-and-return)
|
||||||
- docs/architecture/decisions/004-auth-dispositions.md (the identity
|
- docs/architecture/decisions/004-auth-dispositions.md (the identity
|
||||||
ride), docs/architecture/auth.md §Server direction (step 4)
|
ride), docs/architecture/auth.md §Server direction (step 4)
|
||||||
- docs/research/poc-3-mapping-bakeoff.md §Stage A (the grammar rows,
|
- docs/research/poc-3-mapping-bakeoff.md §Stage A (the grammar rows,
|
||||||
@@ -126,7 +127,8 @@ contract.
|
|||||||
### Decisions recorded (implementation, 2026-10-28)
|
### Decisions recorded (implementation, 2026-10-28)
|
||||||
|
|
||||||
- **Course correction to `w3-seam-config`**: `GitSeam` gains
|
- **Course correction to `w3-seam-config`**: `GitSeam` gains
|
||||||
`limits: alkgit::Limits` — `serve_duplex_resolved` requires the
|
`limits: alkgit::Limits`, since `serve_duplex_resolved` requires
|
||||||
|
the
|
||||||
alkgit ADR-009 session budget, which the landed handle had no field
|
alkgit ADR-009 session budget, which the landed handle had no field
|
||||||
for. alkgit's own contract shapes this ("constructed by the
|
for. alkgit's own contract shapes this ("constructed by the
|
||||||
door/adapter from assembler config"), so the budget rides the seam
|
door/adapter from assembler config"), so the budget rides the seam
|
||||||
@@ -135,34 +137,35 @@ contract.
|
|||||||
docs.
|
docs.
|
||||||
- **The grammar** (`dispatch.rs`, the probe's rows migrated): exact
|
- **The grammar** (`dispatch.rs`, the probe's rows migrated): exact
|
||||||
serving-service token + single separating space + path-shaped
|
serving-service token + single separating space + path-shaped
|
||||||
argument (the wave-2 probe's pinned verdicts preserved verbatim —
|
argument (the wave-2 probe's pinned verdicts preserved verbatim:
|
||||||
`git-upload-pack rel` and double-space arguments are *not git*);
|
`git-upload-pack rel` and double-space arguments are *not git*);
|
||||||
quoted shape is fully `'`-wrapped (unterminated → pipe, the probe's
|
quoted shape is fully `'`-wrapped (unterminated → pipe, the probe's
|
||||||
non-path-shape verdict); leading `/` stripped after quotes; **one
|
non-path-shape verdict); leading `/` stripped after quotes; **one
|
||||||
trailing `.git` suffix stripped** (the vanilla-clone idiom; the
|
trailing `.git` suffix stripped** (the vanilla-clone idiom; the
|
||||||
alkgit registry grammar excludes `.`, so the door normalizes the
|
alkgit registry grammar excludes `.`, so the door normalizes the
|
||||||
idiom rather than asking registries for dotted keys; an empty
|
idiom rather than asking registries for dotted keys; an empty
|
||||||
remainder — `/.git` — is *kept* and the git row's validation
|
remainder (`/.git`) is *kept* and the git row's validation
|
||||||
declines it, never a silent emptying). Validation:
|
declines it, never a silent emptying). Validation:
|
||||||
`alkgit::validate_repo_id` at the git row (`git_map.rs`,
|
`alkgit::validate_repo_id` at the git row (`git_map.rs`,
|
||||||
feature-gated — the parse core stays feature-free); a shape-invalid
|
feature-gated, while the parse core stays feature-free); a
|
||||||
|
shape-invalid
|
||||||
id declines `invalid-repo-id` alkssh-side; a shape-valid id is
|
id declines `invalid-repo-id` alkssh-side; a shape-valid id is
|
||||||
handed through untouched and **alkssh never resolves** (resolution,
|
handed through untouched and **alkssh never resolves** (resolution,
|
||||||
authorize, and the collapsed generic denial live at the seam).
|
authorize, and the collapsed generic denial live at the seam).
|
||||||
Unknown services (`git-send-pack`, `git-upload-archive`) and
|
Unknown services (`git-send-pack`, `git-upload-archive`) and
|
||||||
service-only (including an empty argument) route to
|
service-only (including an empty argument) route to
|
||||||
`ExecRoute::Declined` — build-independent, off the pipe row, per
|
`ExecRoute::Declined` (build-independent, off the pipe row, per
|
||||||
POC-3 §2's honest-surface verdict.
|
POC-3 §2's honest-surface verdict.
|
||||||
- **Wire shapes per decline class** (recorded): SSH request replies
|
- **Wire shapes per decline class** (recorded): SSH request replies
|
||||||
carry no text, so the reason lives only in the event vocabulary;
|
carry no text, so the reason lives only in the event vocabulary;
|
||||||
the git declinables (`unknown-service` / `service-only` /
|
the git declinables (`unknown-service` / `service-only` /
|
||||||
`invalid-repo-id` / `exec-oversize`) share the request-level reply
|
`invalid-repo-id` / `exec-oversize`) share the request-level reply
|
||||||
shape with parse-then-fail — `channel_failure`, channel alive
|
shape with parse-then-fail: `channel_failure`, channel alive
|
||||||
(POC-3's decline shape, POC-1 §5 posture); the reply vocabulary is
|
(POC-3's decline shape, POC-1 §5 posture); the reply vocabulary is
|
||||||
the decision ("unknown service" vs "not git at all" differ in
|
the decision ("unknown service" vs "not git at all" differ in
|
||||||
nothing observable on the wire — only the event). Serving-path
|
nothing observable on the wire, only the event). Serving-path
|
||||||
declines (the row owned the channel) keep failure+close:
|
declines (the row owned the channel) keep failure+close:
|
||||||
`seam-unconfigured` (feature on, seam unset — the unset-declines
|
`seam-unconfigured` (feature on, seam unset, the unset-declines
|
||||||
contract; keeps the wave-2 decline-matrix rows' shape in builds
|
contract; keeps the wave-2 decline-matrix rows' shape in builds
|
||||||
without a configured seam), plus the unreachable-in-practice
|
without a configured seam), plus the unreachable-in-practice
|
||||||
guards `no-identity` and `channel-not-held`. The *serve's own*
|
guards `no-identity` and `channel-not-held`. The *serve's own*
|
||||||
@@ -174,45 +177,46 @@ contract.
|
|||||||
- **Exit mapping** (POC-3 §A.5, decided): `Ok(())` → exit-status(0)
|
- **Exit mapping** (POC-3 §A.5, decided): `Ok(())` → exit-status(0)
|
||||||
(vanilla git exits 0 on success; the clean-end wire row exercises
|
(vanilla git exits 0 on success; the clean-end wire row exercises
|
||||||
the flush-only request's `Ok(None)` → `Ok(())` shape); every
|
the flush-only request's `Ok(None)` → `Ok(())` shape); every
|
||||||
`SubstrateError` → exit-status(1) — `AccessDenied` per the POC's
|
`SubstrateError` → exit-status(1): `AccessDenied` per the POC's
|
||||||
wire evidence, `Io`/`Protocol` the same (the service ran and
|
wire evidence, `Io`/`Protocol` the same (the service ran and
|
||||||
failed), the machinery-misuse variants defensively 1 (they never
|
failed), the machinery-misuse variants defensively 1 (they never
|
||||||
arise from the serving entry). `255` never fabricated (OpenSSH's
|
arise from the serving entry). `255` never fabricated (OpenSSH's
|
||||||
could-not-start semantic this door never claims). Status before
|
could-not-start semantic this door never claims). Status before
|
||||||
close, from the spawned task via the cloned handle; a failed report
|
close, from the spawned task via the cloned handle; a failed report
|
||||||
send is the vocabulary's one `warn` (the wave-2 stub mechanics). A
|
send is the vocabulary's one `warn` (the wave-2 stub mechanics). A
|
||||||
serve `Err` is *not* a decline event — the substrate already mapped
|
serve `Err` is *not* a decline event, as the substrate already
|
||||||
its own error onto the wire.
|
mapped its own error onto the wire.
|
||||||
- **Store-and-take handoff**: `SessionHandler.channels` (plain
|
- **Store-and-take handoff**: `SessionHandler.channels` (plain
|
||||||
`std` HashMap, single-writer — every mutation on the packet loop's
|
`std` HashMap, single-writer, with every mutation on the packet
|
||||||
task). Stored at the confirmed open (accept then insert); taken by
|
loop's task). Stored at the confirmed open (accept then insert); taken by
|
||||||
the git row (`remove` → `into_stream()` in the spawned serving
|
the git row (`remove` → `into_stream()` in the spawned serving
|
||||||
task); released by `channel_close` (the cap is released there too —
|
task); released by `channel_close` (the cap is released there too:
|
||||||
one hook, both budgets). The session rows still hold until their
|
one hook, both budgets). The session rows still hold until their
|
||||||
wave-3 tasks take them. A routed exec answers `channel_success`
|
wave-3 tasks take them. A routed exec answers `channel_success`
|
||||||
inline (the POC-3 §A.5 shape — the request routed; the serving
|
inline (the POC-3 §A.5 shape, where the request routed and the
|
||||||
outcome reports through exit-status).
|
serving outcome reports through exit-status).
|
||||||
- **Identity** (`w2-auth-bridge`'s parked ride-along, discharged):
|
- **Identity** (`w2-auth-bridge`'s parked ride-along, discharged):
|
||||||
the resolved identity is cloned into the serving task and passed on
|
the resolved identity is cloned into the serving task and passed on
|
||||||
`serve_duplex_resolved`'s identity param. The cheapest live witness
|
`serve_duplex_resolved`'s identity param. The cheapest live witness
|
||||||
row: **alkgit's `authorize` evaluation** — two otherwise-identical
|
row: **alkgit's `authorize` evaluation**, giving two
|
||||||
|
otherwise-identical
|
||||||
rows (private repo granted to the riding id → the advertisement
|
rows (private repo granted to the riding id → the advertisement
|
||||||
flows; the same repo ungranted → the denial band + exit 1); no
|
flows; the same repo ungranted → the denial band + exit 1); no
|
||||||
direct identity introspection needed, and the deny row doubles as
|
direct identity introspection needed, and the deny row doubles as
|
||||||
the authorize-path witness. `None` can never occur post-auth and
|
the authorize-path witness. `None` can never occur post-auth and
|
||||||
declines serving-path.
|
declines serving-path.
|
||||||
- **Push semantics through the doubles**: the packless CAS path
|
- **Push semantics through the doubles**: the packless CAS path
|
||||||
(alkgit ADR-020 §1) is the pack-free receive-pack row — commands +
|
(alkgit ADR-020 §1) is the pack-free receive-pack row, with commands +
|
||||||
flush (caps NUL-attached on the first command line, `report-status`
|
flush (caps NUL-attached on the first command line, `report-status`
|
||||||
requested), no pack; the wire layer applies via
|
requested), no pack; the wire layer applies via
|
||||||
`GitRefs::apply_updates` and writes the report (`unpack ok` /
|
`GitRefs::apply_updates` and writes the report (`unpack ok` /
|
||||||
`ok <ref>`). The duplex discipline *drains the raw tail packless*
|
`ok <ref>`). The duplex discipline *drains the raw tail packless*,
|
||||||
— the row had to signal client-side EOF after the report flush
|
which is why the row had to signal client-side EOF after the report
|
||||||
(observed; the real client-eof shape), which is also why vanilla
|
flush (observed, the real client-eof shape) and also why vanilla
|
||||||
delete-pushes end cleanly. `GitPackIngest::prepare` never fires
|
delete-pushes end cleanly. `GitPackIngest::prepare` never fires
|
||||||
packless — asserted.
|
packless, as asserted.
|
||||||
- **Per-build matrix**: unchanged rows in `tests/dispatch_core.rs`
|
- **Per-build matrix**: unchanged rows in `tests/dispatch_core.rs`
|
||||||
(their observables hold — the config-driven decline keeps the
|
(their observables hold, since the config-driven decline keeps the
|
||||||
failure-then-close shape in seam-less builds; unknown services keep
|
failure-then-close shape in seam-less builds; unknown services keep
|
||||||
failure-then-alive), docs/stamps updated for the new decision set;
|
failure-then-alive), docs/stamps updated for the new decision set;
|
||||||
the git row's serving + grammar-decline rows live in the new
|
the git row's serving + grammar-decline rows live in the new
|
||||||
@@ -220,11 +224,11 @@ contract.
|
|||||||
feature isolation). `tests/observe.rs`'s git-row event row updated
|
feature isolation). `tests/observe.rs`'s git-row event row updated
|
||||||
to the `seam-unconfigured` reason.
|
to the `seam-unconfigured` reason.
|
||||||
- **Real-tool smoke posture** (recorded): the clone smoke (a real
|
- **Real-tool smoke posture** (recorded): the clone smoke (a real
|
||||||
`git clone` of an unborn-HEAD repo over a TCP-mounted adapter — the
|
`git clone` of an unborn-HEAD repo over a TCP-mounted adapter, on
|
||||||
same loopback server stack, the accept loop as the assembly's
|
the same loopback server stack, the accept loop as the assembly's
|
||||||
mount) **requires git ≥ 2.43 and ssh in the environment**;
|
mount) **requires git ≥ 2.43 and ssh in the environment**;
|
||||||
missing/older tooling is skipped *with a printed reason* —
|
missing/older tooling is skipped *with a printed reason*
|
||||||
skipped-with-reason at most, never silently green. The empty-repo
|
(skipped-with-reason at most, never silently green). The empty-repo
|
||||||
clone exercises the full chain (exec parse → dispatch → the seam
|
clone exercises the full chain (exec parse → dispatch → the seam
|
||||||
advertisement → ls-refs → exit-status(0)) with zero pack bytes: the
|
advertisement → ls-refs → exit-status(0)) with zero pack bytes: the
|
||||||
unborn-HEAD line is what real git 2.43's clone consumed (POC-3's
|
unborn-HEAD line is what real git 2.43's clone consumed (POC-3's
|
||||||
@@ -233,4 +237,4 @@ contract.
|
|||||||
--features git --test git_mapping`, `cargo test --features session`,
|
--features git --test git_mapping`, `cargo test --features session`,
|
||||||
`cargo test --all-features`, `cargo clippy --all-targets --all-
|
`cargo test --all-features`, `cargo clippy --all-targets --all-
|
||||||
features -- -D warnings` ×2 (default + all-features), `cargo fmt
|
features -- -D warnings` ×2 (default + all-features), `cargo fmt
|
||||||
--check`, `cargo doc --no-deps` — all green.
|
--check`, and `cargo doc --no-deps`: all green.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w3-seam-config
|
id: w3-seam-config
|
||||||
name: Wave 3 — serve-config seam handles (alktty/alkgit per-feature)
|
name: "Wave 3: serve-config seam handles (alktty/alkgit per-feature)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [review-wave-2, w1-config]
|
depends_on: [review-wave-2, w1-config]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -13,7 +13,7 @@ level: implementation
|
|||||||
|
|
||||||
Extend [`crate::ServeConfig`] with the per-feature family-seam handles
|
Extend [`crate::ServeConfig`] with the per-feature family-seam handles
|
||||||
the wave-3 mappings consume. This is wave 3's first *course
|
the wave-3 mappings consume. This is wave 3's first *course
|
||||||
correction* to the wave-1 config surface — the assembly-supplied
|
correction* to the wave-1 config surface: the assembly-supplied
|
||||||
composition inputs were never carried (mapping.md's seam-entries table
|
composition inputs were never carried (mapping.md's seam-entries table
|
||||||
names them inside the seam signatures; the wave-1 config bullet
|
names them inside the seam signatures; the wave-1 config bullet
|
||||||
pre-dates that reading), and the mappings cannot call
|
pre-dates that reading), and the mappings cannot call
|
||||||
@@ -25,17 +25,17 @@ un-set-declines posture structural and the review surface unified:
|
|||||||
rows decline even with the feature on (the decline matrix's "an
|
rows decline even with the feature on (the decline matrix's "an
|
||||||
unset field declines its row though the feature is on" note,
|
unset field declines its row though the feature is on" note,
|
||||||
config-driven). Contents, per alktty 0.8.1's actual
|
config-driven). Contents, per alktty 0.8.1's actual
|
||||||
`drive_session[_pre_negotiated]` signature — decide the exact final
|
`drive_session[_pre_negotiated]` signature; decide the exact final
|
||||||
shape (flat `Option` fields vs an `Option`-struct per seam; record):
|
shape (flat `Option` fields vs an `Option`-struct per seam; record):
|
||||||
- the alktty backends map (`Arc<HashMap<String,
|
- the alktty backends map (`Arc<HashMap<String,
|
||||||
Arc<dyn TtyBackend>>>` — assembled by the assembly; alkssh never
|
Arc<dyn TtyBackend>>>`, assembled by the assembly; alkssh never
|
||||||
touches a real backend, the `local` shapes stay alktty-side);
|
touches a real backend, the `local` shapes stay alktty-side);
|
||||||
- the backend **selector** — the `NegotiateRequest.backend` string
|
- the backend **selector**, the `NegotiateRequest.backend` string
|
||||||
alkssh writes (assembly-supplied, never wire-derived; decide and
|
alkssh writes (assembly-supplied, never wire-derived; decide and
|
||||||
record);
|
record);
|
||||||
- the ownership provider `Option<Arc<dyn
|
- the ownership provider `Option<Arc<dyn
|
||||||
alkcall::core::ownership::OwnershipProvider>>` (an alkcall type —
|
alkcall::core::ownership::OwnershipProvider>>` (an alkcall type,
|
||||||
no new dep; pass-through to `drive_session_pre_negotiated`,
|
so no new dep; pass-through to `drive_session_pre_negotiated`,
|
||||||
family ACL layer riding along).
|
family ACL layer riding along).
|
||||||
- **The `git` seam handle** (feature-gated), unset → the git rows
|
- **The `git` seam handle** (feature-gated), unset → the git rows
|
||||||
decline even with the feature on. Contents, per alkgit 0.1.0's
|
decline even with the feature on. Contents, per alkgit 0.1.0's
|
||||||
@@ -45,15 +45,16 @@ un-set-declines posture structural and the review surface unified:
|
|||||||
and `push_options_negotiated: bool` (the assembler's config flag;
|
and `push_options_negotiated: bool` (the assembler's config flag;
|
||||||
false in the v1 default).
|
false in the v1 default).
|
||||||
- **Manifest** (the wave-1 rule: per-feature sibling deps land with
|
- **Manifest** (the wave-1 rule: per-feature sibling deps land with
|
||||||
their first consuming wave — the config fields consume them first,
|
their first consuming wave, and the config fields consume them
|
||||||
so they are not dead entries): optional deps
|
first, so they are not dead entries): optional deps
|
||||||
`alktty = "0.8.1"`, `alkgit = "0.1.0"` wired feature-to-dep
|
`alktty = "0.8.1"`, `alkgit = "0.1.0"` wired feature-to-dep
|
||||||
(`session = ["dep:alktty"]`, `git = ["dep:alkgit"]`) so a git-build
|
(`session = ["dep:alktty"]`, `git = ["dep:alkgit"]`) so a git-build
|
||||||
never compiles alktty and vice versa; verify the pins against
|
never compiles alktty and vice versa; verify the pins against
|
||||||
crates.io before committing (verified at decomposition: 0.8.1 /
|
crates.io before committing (verified at decomposition: 0.8.1 /
|
||||||
0.1.0, both over the shared `alkcall 0.10` req → 0.10.1; MSRVs 1.88
|
0.1.0, both over the shared `alkcall 0.10` req → 0.10.1; MSRVs 1.88
|
||||||
≤ our 1.89; MIT OR Apache-2.0). **alktunnels is NOT landed here** —
|
≤ our 1.89; MIT OR Apache-2.0). **alktunnels is NOT landed here**,
|
||||||
the config carries no alktunnels types beyond the already-landed
|
since the config carries no alktunnels types beyond the
|
||||||
|
already-landed
|
||||||
`Arc<ChannelClient>`; its optional dep lands with its first
|
`Arc<ChannelClient>`; its optional dep lands with its first
|
||||||
consuming code (`w3-tunnel-direct`).
|
consuming code (`w3-tunnel-direct`).
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ each seam handle carries).
|
|||||||
- `Cargo.toml`: optional `alktty = "0.8.1"` /
|
- `Cargo.toml`: optional `alktty = "0.8.1"` /
|
||||||
`alkgit = "0.1.0"` (both `default-features = false`), wired
|
`alkgit = "0.1.0"` (both `default-features = false`), wired
|
||||||
`session = ["dep:alktty"]`, `git = ["dep:alkgit"]`. alktty's `local`
|
`session = ["dep:alktty"]`, `git = ["dep:alkgit"]`. alktty's `local`
|
||||||
feature stays off — alkssh never touches a real backend; the `local`
|
feature stays off, since alkssh never touches a real backend; the `local`
|
||||||
shapes stay alktty-side. Feature-graph comment updated (wave-3
|
shapes stay alktty-side. Feature-graph comment updated (wave-3
|
||||||
seam-config is the deps' first consumer; alktunnels still lands with
|
seam-config is the deps' first consumer; alktunnels still lands with
|
||||||
`w3-tunnel-direct`).
|
`w3-tunnel-direct`).
|
||||||
@@ -114,18 +115,20 @@ each seam handle carries).
|
|||||||
`GitSeam{registry, refs, packgen, ingest, push_options_negotiated,
|
`GitSeam{registry, refs, packgen, ingest, push_options_negotiated,
|
||||||
pipeline}` held as `cfg`-gated `Option` fields
|
pipeline}` held as `cfg`-gated `Option` fields
|
||||||
(`session_seam` / `git_seam`), each `Clone` (all payload types `Arc`
|
(`session_seam` / `git_seam`), each `Clone` (all payload types `Arc`
|
||||||
or plain data — the spawned serving tasks clone the seam), each with a
|
or plain data, as the spawned serving tasks clone the seam), each with a
|
||||||
hand-rolled `Debug`. **Flat `Option` fields were rejected**: each seam
|
hand-rolled `Debug`. **Flat `Option` fields were rejected**: each seam
|
||||||
is consumed atomically by exactly one seam call
|
is consumed atomically by exactly one seam call
|
||||||
(`drive_session[_pre_negotiated]` / `serve_duplex_resolved`), so a
|
(`drive_session[_pre_negotiated]` / `serve_duplex_resolved`), so a
|
||||||
struct admits no half-supplied seam states (backends without a
|
struct admits no half-supplied seam states (backends without a
|
||||||
selector; three traits without the fourth) that would all map to the
|
selector; three traits without the fourth) that would all map to the
|
||||||
same all-or-nothing decline — the unset-declines contract reads as one
|
same all-or-nothing decline, which is why the unset-declines contract
|
||||||
|
reads as one
|
||||||
`Option` per seam, "unset → the owned rows decline even though the
|
`Option` per seam, "unset → the owned rows decline even though the
|
||||||
feature is on" (the decline matrix's config-driven note), checked at
|
feature is on" (the decline matrix's config-driven note), checked at
|
||||||
request time by the mappings, never at construction. The already-landed
|
request time by the mappings, never at construction. The already-landed
|
||||||
tunnel handle stays the flat
|
tunnel handle stays the flat
|
||||||
`Option<Arc<ChannelClient>>` — a single-field seam needs no struct.
|
`Option<Arc<ChannelClient>>`, since a single-field seam needs no
|
||||||
|
struct.
|
||||||
Naming: struct names carry the crate (the consuming mappings will see
|
Naming: struct names carry the crate (the consuming mappings will see
|
||||||
it in the field docs); the fields carry the row vocabulary
|
it in the field docs); the fields carry the row vocabulary
|
||||||
(`session_seam` / `git_seam`, matching the decline matrix's row names).
|
(`session_seam` / `git_seam`, matching the decline matrix's row names).
|
||||||
@@ -134,16 +137,16 @@ it in the field docs); the fields carry the row vocabulary
|
|||||||
|
|
||||||
- `alktty 0.8.1`: published 2026-10-22, not yanked,
|
- `alktty 0.8.1`: published 2026-10-22, not yanked,
|
||||||
`MIT OR Apache-2.0`, `rust_version = "1.88"` ≤ our 1.89;
|
`MIT OR Apache-2.0`, `rust_version = "1.88"` ≤ our 1.89;
|
||||||
`alkcall ^0.10.1` — shares our `alkcall = "0.10"` resolution
|
`alkcall ^0.10.1`, which shares our `alkcall = "0.10"` resolution
|
||||||
(0.10.1), so no alkcall fork in the lockfile.
|
(0.10.1), so no alkcall fork in the lockfile.
|
||||||
- `alkgit 0.1.0`: `MIT OR Apache-2.0`, `rust_version = "1.88"`;
|
- `alkgit 0.1.0`: `MIT OR Apache-2.0`, `rust_version = "1.88"`;
|
||||||
`alkcall ^0.10` — same shared resolution.
|
`alkcall ^0.10`, the same shared resolution.
|
||||||
- Signatures confirmed against the published crates (the registry
|
- Signatures confirmed against the published crates (the registry
|
||||||
sources under `~/.cargo/registry/src/`): alktty's
|
sources under `~/.cargo/registry/src/`): alktty's
|
||||||
`drive_session[_pre_negotiated]` (adapter.rs:205-241) takes
|
`drive_session[_pre_negotiated]` (adapter.rs:205-241) takes
|
||||||
`(writer, reader, [req,] backends: Arc<HashMap<String, Arc<dyn
|
`(writer, reader, [req,] backends: Arc<HashMap<String, Arc<dyn
|
||||||
TtyBackend>>>, ownership: Option<Arc<dyn OwnershipProvider>>,
|
TtyBackend>>>, ownership: Option<Arc<dyn OwnershipProvider>>,
|
||||||
identity: Option<Identity>)` — the identity is the per-connection
|
identity: Option<Identity>)`, where the identity is the per-connection
|
||||||
resolved one, not config; alkgit's `serve_duplex_resolved`
|
resolved one, not config; alkgit's `serve_duplex_resolved`
|
||||||
(duplex.rs:1412) takes the four traits +
|
(duplex.rs:1412) takes the four traits +
|
||||||
`push_options_negotiated: bool` + `pipeline: Arc<Semaphore>`.
|
`push_options_negotiated: bool` + `pipeline: Arc<Semaphore>`.
|
||||||
@@ -154,7 +157,7 @@ it in the field docs); the fields carry the row vocabulary
|
|||||||
### One course correction absorbed: `ObjectId` is not re-exported by alkgit
|
### One course correction absorbed: `ObjectId` is not re-exported by alkgit
|
||||||
|
|
||||||
`GitPackGen::generate`/`common_haves` name `Vec<gix_hash::ObjectId>`
|
`GitPackGen::generate`/`common_haves` name `Vec<gix_hash::ObjectId>`
|
||||||
and alkgit's lib root does not re-export `ObjectId` — a backend-trait
|
and alkgit's lib root does not re-export `ObjectId`; a backend-trait
|
||||||
doubled in tests has to name the type, so `gix-hash = "0.28"` (matching
|
doubled in tests has to name the type, so `gix-hash = "0.28"` (matching
|
||||||
alkgit's `^0.28` req; the same crate already in the lockfile) lands as
|
alkgit's `^0.28` req; the same crate already in the lockfile) lands as
|
||||||
a **`[dev-dependencies]` entry only**, exercised exclusively by the
|
a **`[dev-dependencies]` entry only**, exercised exclusively by the
|
||||||
@@ -163,16 +166,15 @@ feature-to-dep isolation below; the dev graph is universal by design
|
|||||||
(tokio and friends are too). Flagged for `w3-git-mapping`: the
|
(tokio and friends are too). Flagged for `w3-git-mapping`: the
|
||||||
hand-rolled in-mem quartet will lean on this dev-dep for `ObjectId`
|
hand-rolled in-mem quartet will lean on this dev-dep for `ObjectId`
|
||||||
(same as this task's noop doubles, modeled on alkgit's own
|
(same as this task's noop doubles, modeled on alkgit's own
|
||||||
`backend.rs:164-254` noop shapes — own code, not copied).
|
`backend.rs:164-254` noop shapes; own code, not copied).
|
||||||
|
|
||||||
### Dependency isolation — verified structurally, not by assertion
|
### Dependency isolation, verified structurally not by assertion
|
||||||
|
|
||||||
`cargo tree -e features --features git`: zero `alktty` occurrences;
|
`cargo tree -e features --features git`: zero `alktty` occurrences;
|
||||||
`cargo tree -e features --features session`: zero `alkgit`
|
`cargo tree -e features --features session`: zero `alkgit`
|
||||||
occurrences; `--all-features`: both present. Combined with the cfg
|
occurrences; `--all-features`: both present. Combined with the cfg
|
||||||
gates (all `alktty`/`alkgit` type references live inside
|
gates (all `alktty`/`alkgit` type references live inside
|
||||||
`#[cfg(feature = "session")]` / `#[cfg(feature = "git")]` blocks —
|
`#[cfg(feature = "session")]` / `#[cfg(feature = "git")]` blocks (imports, fields, accessors, Debug, constructor, tests), the "a
|
||||||
imports, fields, accessors, Debug, constructor, tests), the "a
|
|
||||||
git-build never compiles alktty and vice versa" invariant is
|
git-build never compiles alktty and vice versa" invariant is
|
||||||
compile-proven for both consumption sides.
|
compile-proven for both consumption sides.
|
||||||
|
|
||||||
@@ -181,7 +183,8 @@ compile-proven for both consumption sides.
|
|||||||
The wave-3 mappings own the request-time decline checks; this task
|
The wave-3 mappings own the request-time decline checks; this task
|
||||||
lands the config-level contract rows (constructor posture, `Option`
|
lands the config-level contract rows (constructor posture, `Option`
|
||||||
configured() semantics, seam-vs-OQ-08-field independence, Debug
|
configured() semantics, seam-vs-OQ-08-field independence, Debug
|
||||||
posture — 8 unit rows across the two feature modules) and leaves the
|
posture, making 8 unit rows across the two feature modules) and
|
||||||
|
leaves the
|
||||||
wave-2 harness rows untouched (featured-but-unset already declines at
|
wave-2 harness rows untouched (featured-but-unset already declines at
|
||||||
the row the stubs own; the real serving paths make those rows
|
the row the stubs own; the real serving paths make those rows
|
||||||
meaningful in `w3-git-mapping` / `w3-tty-session`, whose seam
|
meaningful in `w3-git-mapping` / `w3-tty-session`, whose seam
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w3-tty-session
|
id: w3-tty-session
|
||||||
name: Wave 3 — session → alktty (translation bridge + exit derivation)
|
name: "Wave 3: session → alktty (translation bridge + exit derivation)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w3-git-mapping]
|
depends_on: [w3-git-mapping]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -11,86 +11,86 @@ level: implementation
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the session branch — the biggest single-row surface of the spine
|
Land the session branch, the biggest single-row surface of the spine
|
||||||
(mapping.md's `session` rows): pty/shell/pipe-exec become a real
|
(mapping.md's `session` rows): pty/shell/pipe-exec become a real
|
||||||
serving path through the published alktty seam, as a **translation
|
serving path through the published alktty seam, as a **translation
|
||||||
bridge, not a byte pump** (POC-3 §D.1). Everything POC-3 stage D
|
bridge, not a byte pump** (POC-3 §D.1). Everything POC-3 stage D
|
||||||
proved, re-landed as contract:
|
proved, re-landed as contract:
|
||||||
|
|
||||||
- **The per-channel state + the store-and-take reuse** — the channel
|
- **The per-channel state + the store-and-take reuse**: the channel
|
||||||
store `w3-git-mapping` lands (`channels.remove(&id)` →
|
store `w3-git-mapping` lands (`channels.remove(&id)` →
|
||||||
`into_stream()` in the spawned serving task) gains the session
|
`into_stream()` in the spawned serving task) gains the session
|
||||||
rows' per-channel state: the PTY params a `pty_request` delivered
|
rows' per-channel state, the PTY params a `pty_request` delivered
|
||||||
(consumed by the later `shell_request`/`exec_request` on the same
|
(consumed by the later `shell_request`/`exec_request` on the same
|
||||||
channel — `ssh -t host "cmd"` sends pty-then-exec and must serve
|
channel, since `ssh -t host "cmd"` sends pty-then-exec and must
|
||||||
PTY-mode; a `shell_request` without a preceding pty serves
|
serve in PTY-mode; a `shell_request` without a preceding pty serves
|
||||||
**pipe-mode** `tty = None` — decide whether such a shell serves or
|
**pipe-mode** `tty = None`, and whether such a shell serves or
|
||||||
declines and record).
|
declines is for the task to decide and record).
|
||||||
- **Negotiation, built locally, never wire-derived** (OQ-08 — the
|
- **Negotiation, built locally, never wire-derived** (OQ-08: the
|
||||||
serve-config is the whole truth): `pty_request` args →
|
serve-config is the whole truth): `pty_request` args →
|
||||||
`TerminalParamsWire{term, cols, rows, pixel_w/h, modes}` (SSH
|
`TerminalParamsWire{term, cols, rows, pixel_w/h, modes}` (SSH
|
||||||
ttymodes → the reserved modes JSON — alktty OQ-44's reservation,
|
ttymodes → the reserved modes JSON, which is alktty OQ-44's
|
||||||
deferred there); `shell_request` → `NegotiateRequest{carriage:
|
reservation, deferred there); `shell_request` → `NegotiateRequest{carriage:
|
||||||
"raw", cmd: shell_argv (the serve-config shell, verbatim), cwd:
|
"raw", cmd: shell_argv (the serve-config shell, verbatim), cwd:
|
||||||
session_cwd, env: session_env, backend: the seam selector}` →
|
session_cwd, env: session_env, backend: the seam selector}` →
|
||||||
**`drive_session_pre_negotiated(writer, reader, req, backends,
|
**`drive_session_pre_negotiated(writer, reader, req, backends,
|
||||||
ownership, identity)`** spawned; `exec_request` (pipe row) → `cmd =
|
ownership, identity)`** spawned; `exec_request` (pipe row) → `cmd =
|
||||||
wrapper ++ [cmd]` (the OQ-08 argv append — vanilla quoting
|
wrapper ++ [cmd]` (the OQ-08 argv append with vanilla quoting
|
||||||
semantics; **never naively split**), `tty = None` pipe mode.
|
semantics; **never naively split**), `tty = None` pipe mode.
|
||||||
**Unset-declines** (OQ-08): `shell_argv` unset → `shell_request`
|
**Unset-declines** (OQ-08): `shell_argv` unset → `shell_request`
|
||||||
declines; `exec_wrapper_argv` unset → the pipe row declines — never
|
declines; `exec_wrapper_argv` unset → the pipe row declines (never
|
||||||
a fallback. Decide the wire shape per row (capability decline —
|
a fallback). Decide the wire shape per row (capability decline,
|
||||||
channel stays usable — vs the serving-path failure+close shape) and
|
channel stays usable, vs the serving-path failure+close shape) and
|
||||||
record.
|
record.
|
||||||
- **The translation bridge** — two small adapters around a duplex the
|
- **The translation bridge**, two small adapters around a duplex the
|
||||||
serving task drives (the POC-3 §D.1 shape): SSH `data`/EOF →
|
serving task drives (the POC-3 §D.1 shape): SSH `data`/EOF →
|
||||||
`Chunk::stdin` (decide the EOF translation — the zero-length
|
`Chunk::stdin` (decide the EOF translation: the zero-length
|
||||||
stdin-chunk sentinel vs alktty's `ControlMessage::Eof` CTRL_IN —
|
stdin-chunk sentinel vs alktty's `ControlMessage::Eof` CTRL_IN;
|
||||||
verify what alktty's ChunkReader/backend actually consumes and
|
verify what alktty's ChunkReader/backend actually consumes and
|
||||||
record); stdout chunks → SSH `data`; stderr chunks →
|
record); stdout chunks → SSH `data`; stderr chunks →
|
||||||
`extended_data(1)` (PTY mode merges upstream — the backend's
|
`extended_data(1)` (PTY mode merges upstream, where the backend's
|
||||||
stderr stream is `None` there, so the translation is universal, no
|
stderr stream is `None`, so the translation is universal with no
|
||||||
mode-switch); the exit chunk (`STREAM_CTRL_OUT`) → the derivation
|
mode-switch); the exit chunk (`STREAM_CTRL_OUT`) → the derivation
|
||||||
contract. No unbounded buffering in either adapter (convention 9).
|
contract. No unbounded buffering in either adapter (convention 9).
|
||||||
- **Control rows** — `window_change_request` → `ControlMessage::
|
- **Control rows**: `window_change_request` → `ControlMessage::
|
||||||
Resize` CTRL_IN; `signal` → `ControlMessage::Signal` (alktty's
|
Resize` CTRL_IN; `signal` → `ControlMessage::Signal` (alktty's
|
||||||
uppercase-name vocabulary; name accepted verbatim — decide the
|
uppercase-name vocabulary, with the name accepted verbatim; decide
|
||||||
decline shape for names alktty's backend semantics reject and
|
the decline shape for names alktty's backend semantics reject and
|
||||||
record). Mechanics: the requests arrive on the packet loop after
|
record). Mechanics: the requests arrive on the packet loop after
|
||||||
the serving task owns the wire — reach them into the spawned bridge
|
the serving task owns the wire, so reach them into the spawned
|
||||||
through a per-channel control surface (bounded mpsc — decide +
|
bridge through a per-channel control surface (a bounded mpsc; decide
|
||||||
record; a silent unbounded proxy is a bounded-resources bug).
|
plus record; a silent unbounded proxy is a bounded-resources bug).
|
||||||
- **The exit derivation contract** (mapping.md rules 1–6,
|
- **The exit derivation contract** (mapping.md rules 1–6,
|
||||||
normative): the alktty exit chunk (`{"type":"exit","code":i32}`,
|
normative): the alktty exit chunk (`{"type":"exit","code":i32}`,
|
||||||
exit-chunk-is-last) is the sole fidelity input, never widened:
|
exit-chunk-is-last) is the sole fidelity input, never widened:
|
||||||
`code ≥ 0` → `exit-status(N)`; negative `−N`, N ≥ 2 →
|
`code ≥ 0` → `exit-status(N)`; negative `−N`, N ≥ 2 →
|
||||||
`exit-signal(name)` via the **server-side Unix signum→name table**
|
`exit-signal(name)` via the **server-side Unix signum→name table**
|
||||||
(crate-local; not derived from alktty's `signal_from_name` inverse
|
(crate-local; not derived from alktty's `signal_from_name` inverse
|
||||||
unless that is public and sound — decide the table's form and
|
unless that is public and sound; decide the table's form and
|
||||||
record; `core_dumped` **never asserted**, the bool stays false);
|
record; `core_dumped` **never asserted**, the bool stays false);
|
||||||
`−1` → `exit-status(255)`, never a signal claim (rule 3 — the
|
`−1` → `exit-status(255)`, never a signal claim (rule 3, the
|
||||||
SIGHUP collision accepted symmetrically, per the mapping.md
|
SIGHUP collision accepted symmetrically per the mapping.md
|
||||||
rationale); non-Unix adds no other negatives (rule 4); the PTY-path
|
rationale); non-Unix adds no other negatives (rule 4); the PTY-path
|
||||||
rule-5 fidelity limit documented in the module docs (signal-
|
rule-5 fidelity limit documented in the module docs (signal-
|
||||||
terminated PTY sessions arrive `exit-status(1)` — portable-pty's
|
terminated PTY sessions arrive `exit-status(1)`, portable-pty's
|
||||||
exit surface, upstream); unmapped negatives → 255 (rule 6). Sent
|
exit surface, upstream); unmapped negatives → 255 (rule 6). Sent
|
||||||
**before** close, from the spawned task.
|
**before** close, from the spawned task.
|
||||||
- **`env_request` stays declined** (OQ-09 — unchanged from wave 2);
|
- **`env_request` stays declined** (OQ-09, unchanged from wave 2);
|
||||||
`session_cwd`/`session_env` ride `NegotiateRequest` only.
|
`session_cwd`/`session_env` ride `NegotiateRequest` only.
|
||||||
- **Contract rows through the harness, per build** — the wave-2 stub
|
- **Contract rows through the harness, per build**, with the wave-2
|
||||||
rows (`pty_row_stub`/`shell_row_stub`/
|
stub rows (`pty_row_stub`/`shell_row_stub`/
|
||||||
`pipe_row_stub`) replaced; the decline matrix's unset-field rows —
|
`pipe_row_stub`) replaced; the decline matrix's unset-field rows
|
||||||
feature on + handle unset → the rows decline; featured+set serve
|
(feature on + handle unset → the rows decline; featured+set serves
|
||||||
through the alkssh-side **TtyBackend shim double** (alktty's
|
through the alkssh-side **TtyBackend shim double**, since alktty's
|
||||||
`MockBackend`/`testing.rs` are `pub(crate)` + `cfg(test)` — never
|
`MockBackend`/`testing.rs` are `pub(crate)` + `cfg(test)` and so
|
||||||
importable; alkssh hand-rolls the shim: a backend that records
|
never importable; alkssh hand-rolls the shim, a backend that
|
||||||
`TtyParams`, emits scripted stdout/stderr/exit chunks and consumes
|
records `TtyParams`, emits scripted stdout/stderr/exit chunks and
|
||||||
stdin/resizes/signals — per the `TtyHandle` field shapes). The
|
consumes stdin/resizes/signals, per the `TtyHandle` field shapes). The
|
||||||
identity-landing item (the parked wave-2 note) asserted through the
|
identity-landing item (the parked wave-2 note) asserted through the
|
||||||
row that observes the identity at `drive_session_pre_negotiated`.
|
row that observes the identity at `drive_session_pre_negotiated`.
|
||||||
- **The real-tool smoke** — one OpenSSH pty session through the
|
- **The real-tool smoke**: one OpenSSH pty session through the
|
||||||
harness against the shim (or against a real PTY via a dev-dep
|
harness against the shim (or against a real PTY via a dev-dep
|
||||||
`alktty` `local` composition — decide which posture and record; the
|
`alktty` `local` composition; decide which posture and record; the
|
||||||
real bash/`stty size` fidelity was POC stage D's, wave 5 re-lands it
|
real bash/`stty size` fidelity was POC stage D's, wave 5 re-lands it
|
||||||
as e2e): the smoke owns the wire rows a plain rustsh-client row
|
as e2e): the smoke owns the wire rows a plain rustsh-client row
|
||||||
can't pin (echo paths, `stty size`-grade fidelity are wave-5's
|
can't pin (echo paths, `stty size`-grade fidelity are wave-5's
|
||||||
@@ -112,7 +112,7 @@ proved, re-landed as contract:
|
|||||||
per-channel surface)
|
per-channel surface)
|
||||||
- [x] The derivation contract rules 1–6 in code with the signum→name
|
- [x] The derivation contract rules 1–6 in code with the signum→name
|
||||||
table; `−1`→255; `core_dumped` never; PTY-path rule-5 documented;
|
table; `−1`→255; `core_dumped` never; PTY-path rule-5 documented;
|
||||||
exit-status before close from the spawned task — pinned by rows
|
exit-status before close from the spawned task, pinned by rows
|
||||||
- [x] The decline matrix per build green through the harness (unset-
|
- [x] The decline matrix per build green through the harness (unset-
|
||||||
field rows included); identity ride asserted; the `env_request`
|
field rows included); identity ride asserted; the `env_request`
|
||||||
decline unchanged; skeleton stub rows migrated
|
decline unchanged; skeleton stub rows migrated
|
||||||
@@ -123,16 +123,16 @@ proved, re-landed as contract:
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- docs/architecture/mapping.md (the `session` channel-type row,
|
- docs/architecture/mapping.md (the `session` channel-type row,
|
||||||
request-level rows, §Exit chunk → SSH exit reporting — rules 1–6)
|
request-level rows, §Exit chunk → SSH exit reporting, rules 1–6)
|
||||||
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
||||||
(decision 4 — spawn-and-return: status before close from the spawned
|
(decision 4, spawn-and-return: status before close from the spawned
|
||||||
task), docs/architecture/decisions/003-feature-graph.md
|
task), docs/architecture/decisions/003-feature-graph.md
|
||||||
- docs/architecture/auth.md §Server direction step 4 (the identity
|
- docs/architecture/auth.md §Server direction step 4 (the identity
|
||||||
ride), §Dispositions (OQ-03)
|
ride), §Dispositions (OQ-03)
|
||||||
- docs/research/poc-3-mapping-bakeoff.md §Stage D (the bridge shape,
|
- docs/research/poc-3-mapping-bakeoff.md §Stage D (the bridge shape,
|
||||||
control rows, the signal/exit fidelity findings, §D.3 mechanics)
|
control rows, the signal/exit fidelity findings, §D.3 mechanics)
|
||||||
- docs/research/reverse-runner.md (pipe-mode pid-only signal caveat —
|
- docs/research/reverse-runner.md (the pipe-mode pid-only signal
|
||||||
the fidelity note)
|
caveat, the fidelity note)
|
||||||
- alktty 0.8.1: `adapter.rs:234-241` (`drive_session_pre_negotiated`),
|
- alktty 0.8.1: `adapter.rs:234-241` (`drive_session_pre_negotiated`),
|
||||||
`negotiation.rs` (`NegotiateRequest`/`TerminalParamsWire`),
|
`negotiation.rs` (`NegotiateRequest`/`TerminalParamsWire`),
|
||||||
`wire.rs` (chunks, readers/writers), `control.rs` (`ControlMessage`),
|
`wire.rs` (chunks, readers/writers), `control.rs` (`ControlMessage`),
|
||||||
@@ -143,31 +143,32 @@ proved, re-landed as contract:
|
|||||||
### Decisions recorded (implementation, 2026-10-28)
|
### Decisions recorded (implementation, 2026-10-28)
|
||||||
|
|
||||||
- **The module** (`src/tty_map.rs`, feature `session`): the session
|
- **The module** (`src/tty_map.rs`, feature `session`): the session
|
||||||
rows live in one module following the `git_map` pattern — the
|
rows live in one module following the `git_map` pattern, covering
|
||||||
bridge, the locally-built negotiation, the derivation contract, and
|
the bridge, the locally-built negotiation, the derivation contract,
|
||||||
|
and
|
||||||
the serving path. `serde_json` (the OQ-44 modes JSON + the exit
|
the serving path. `serde_json` (the OQ-44 modes JSON + the exit
|
||||||
chunk/error-frame reads) and `bytes` (the chunk plane) joined as
|
chunk/error-frame reads) and `bytes` (the chunk plane) joined as
|
||||||
session-gated optional deps at alktty's own pins.
|
session-gated optional deps at alktty's own pins.
|
||||||
|
|
||||||
- **The negotiation** (locally built, never wire-derived — OQ-08):
|
- **The negotiation** (locally built, never wire-derived; OQ-08):
|
||||||
`pty_request` args → `TerminalParamsWire` with SSH ttymodes → the
|
`pty_request` args → `TerminalParamsWire` with SSH ttymodes → the
|
||||||
**reserved `{}` modes JSON** (alktty OQ-44's reservation, deferred
|
**reserved `{}` modes JSON** (alktty OQ-44's reservation, deferred
|
||||||
there; the map is honest — nothing is carried). Shell argv rides the
|
there; the map is honest and nothing is carried). Shell argv rides the
|
||||||
serve-config **verbatim**; exec rides `wrapper ++ [cmd]` (never
|
serve-config **verbatim**; exec rides `wrapper ++ [cmd]` (never
|
||||||
naively split); cwd/env come from `session_cwd`/`session_env` only.
|
naively split); cwd/env come from `session_cwd`/`session_env` only.
|
||||||
`backend_params` stays empty (no backend-specific selector fields on
|
`backend_params` stays empty (no backend-specific selector fields on
|
||||||
this door; the seam selector is the `backend` string).
|
this door; the seam selector is the `backend` string).
|
||||||
**Pipe-less shell decided: a `shell_request` without a preceding
|
**Pipe-less shell decided: a `shell_request` without a preceding
|
||||||
pty SERVES pipe-mode (`tty = None`)** — vanilla sshd serves a shell
|
pty SERVES pipe-mode (`tty = None`)**, because vanilla sshd serves
|
||||||
without a terminal too (`ssh -T` is legitimate vanilla).
|
a shell without a terminal too (`ssh -T` is legitimate vanilla).
|
||||||
|
|
||||||
- **Unset-declines (OQ-08) — wire shape decided + recorded**: the
|
- **Unset-declines (OQ-08), wire shape decided + recorded**: the
|
||||||
**serving-path shape (`channel_failure` then close, from a spawned
|
**serving-path shape (`channel_failure` then close, from a spawned
|
||||||
task)** for `shell_argv`-unset (`shell-unconfigured`),
|
task)** for `shell_argv`-unset (`shell-unconfigured`),
|
||||||
`exec_wrapper_argv`-unset (`wrapper-unconfigured`), and the
|
`exec_wrapper_argv`-unset (`wrapper-unconfigured`), and the
|
||||||
`session_seam`-unset rows (`seam-unconfigured` — the git row's
|
`session_seam`-unset rows (`seam-unconfigured`, the git row's
|
||||||
recorded shape); the pty row checks the seam first (a doomed
|
recorded shape); the pty row checks the seam first (a doomed
|
||||||
allocation is declined before its params are stored — decline first,
|
allocation is declined before its params are stored (decline first,
|
||||||
alloc never). Field checks precede the seam check. The unvanilla
|
alloc never). Field checks precede the seam check. The unvanilla
|
||||||
guards keep the git row's shapes: `channel-not-held` → inline
|
guards keep the git row's shapes: `channel-not-held` → inline
|
||||||
request-level failure; `no-identity` → the spawned serving-path
|
request-level failure; `no-identity` → the spawned serving-path
|
||||||
@@ -177,7 +178,7 @@ proved, re-landed as contract:
|
|||||||
- **The translation bridge** (the POC-3 §D.1 shape, two adapters
|
- **The translation bridge** (the POC-3 §D.1 shape, two adapters
|
||||||
around a duplex pair): SSH `data` → `Chunk::stdin` through a bounded
|
around a duplex pair): SSH `data` → `Chunk::stdin` through a bounded
|
||||||
mpsc (bound 64) feeding a framing task; SSH channel EOF → the
|
mpsc (bound 64) feeding a framing task; SSH channel EOF → the
|
||||||
**zero-length stdin-chunk sentinel** — decided and verified against
|
**zero-length stdin-chunk sentinel**, decided and verified against
|
||||||
alktty 0.8.1's `pump_client_to_backend`: a zero-length stdin chunk
|
alktty 0.8.1's `pump_client_to_backend`: a zero-length stdin chunk
|
||||||
shuts the backend's stdin, identical in effect to
|
shuts the backend's stdin, identical in effect to
|
||||||
`ControlMessage::Eof`, and rides the data path the SSH EOF already
|
`ControlMessage::Eof`, and rides the data path the SSH EOF already
|
||||||
@@ -189,31 +190,32 @@ proved, re-landed as contract:
|
|||||||
- **The negotiation-error frame is a serving-path decline class**
|
- **The negotiation-error frame is a serving-path decline class**
|
||||||
(decided + recorded): alktty re-runs validation/allocation/ownership
|
(decided + recorded): alktty re-runs validation/allocation/ownership
|
||||||
inline even on the pre-negotiated path and answers failures with a
|
inline even on the pre-negotiated path and answers failures with a
|
||||||
length-prefixed error frame (first byte `0x00` — alktty's ADR-052 §5
|
length-prefixed error frame (first byte `0x00`, which is alktty's
|
||||||
disambiguation invariant). The bridge reads it via a 4-byte
|
ADR-052 §5 disambiguation invariant). The bridge reads it via a 4-byte
|
||||||
first-window disambiguation and maps it to `negotiation-failed` +
|
first-window disambiguation and maps it to `negotiation-failed` +
|
||||||
**`exit-status(255)` + close — the one place this door claims 255**:
|
**`exit-status(255)` + close, the one place this door claims 255**:
|
||||||
the session never started, nothing ran, so 255 is the honest
|
the session never started, nothing ran, so 255 is the honest
|
||||||
could-not-start semantic (contrast the git row, whose service runs
|
could-not-start semantic (contrast the git row, whose service runs
|
||||||
and honestly reports `1`; `255` is never fabricated for a real
|
and honestly reports `1`; `255` is never fabricated for a real
|
||||||
outcome). A bridge read ending **without** an exit chunk reports
|
outcome). A bridge read ending **without** an exit chunk reports
|
||||||
nothing (sole-fidelity-input posture: absence is neither 0 nor 255)
|
nothing (sole-fidelity-input posture: absence is neither 0 nor 255),
|
||||||
— plain close.
|
and simply closes.
|
||||||
|
|
||||||
- **The control rows**: `window_change_request` →
|
- **The control rows**: `window_change_request` →
|
||||||
`ControlMessage::Resize`, `signal` → `ControlMessage::Signal`,
|
`ControlMessage::Resize`, `signal` → `ControlMessage::Signal`,
|
||||||
forwarded through the **per-channel `BridgeSurface`** (handler
|
forwarded through the **per-channel `BridgeSurface`** (handler
|
||||||
state while the row serves): a bounded mpsc whose `try_send` is
|
state while the row serves): a bounded mpsc whose `try_send` is
|
||||||
never awaited on the packet loop — a full/closed surface drops the
|
never awaited on the packet loop, so a full/closed surface drops
|
||||||
control event (fire-and-forget rows carry no SSH reply to decline
|
the control event (fire-and-forget rows carry no SSH reply to decline
|
||||||
with; bounded resources, convention 9). **Signal names are accepted
|
with; bounded resources, convention 9). **Signal names are accepted
|
||||||
verbatim — no decline shape**: the backend owns the rejection
|
verbatim, no decline shape**: the backend owns the rejection
|
||||||
semantics, nothing on the wire can answer. A pre-serve resize
|
semantics, nothing on the wire can answer. A pre-serve resize
|
||||||
updates the stored pty params instead (the final size wins —
|
updates the stored pty params instead (the final size wins,
|
||||||
vanilla sshd's ordering).
|
matching vanilla sshd's ordering).
|
||||||
|
|
||||||
- **The derivation contract** (mapping.md rules 1–6, in code + unit
|
- **The derivation contract** (mapping.md rules 1–6, in code + unit
|
||||||
rows + wire rows): `derive_exit` — `code ≥ 0` → `exit-status(N)`;
|
rows + wire rows): `derive_exit`, where `code ≥ 0` →
|
||||||
|
`exit-status(N)`;
|
||||||
negative `−N`, N ≥ 2 → `exit-signal(name)` via the crate-local
|
negative `−N`, N ≥ 2 → `exit-signal(name)` via the crate-local
|
||||||
[`SIGNAL_NAMES`] signum→name table (1–31, server-side work,
|
[`SIGNAL_NAMES`] signum→name table (1–31, server-side work,
|
||||||
deliberately not alktty's `signal_from_name` inverse); `−1` →
|
deliberately not alktty's `signal_from_name` inverse); `−1` →
|
||||||
@@ -223,22 +225,22 @@ proved, re-landed as contract:
|
|||||||
`i32::MIN`'s checked-abs failure) → 255 (rule 6); the PTY-path
|
`i32::MIN`'s checked-abs failure) → 255 (rule 6); the PTY-path
|
||||||
rule-5 fidelity limit documented in the module docs. Report sent
|
rule-5 fidelity limit documented in the module docs. Report sent
|
||||||
**before** close from the spawned task; names ride russh's `Sig`
|
**before** close from the spawned task; names ride russh's `Sig`
|
||||||
(known variants verbatim, `Sig::Custom` for the rest — RFC 4254
|
(known variants verbatim, `Sig::Custom` for the rest; RFC 4254
|
||||||
§6.10).
|
§6.10).
|
||||||
|
|
||||||
- **Cancellation** (bounded resources + alktty's kill-on-drop): the
|
- **Cancellation** (bounded resources + alktty's kill-on-drop): the
|
||||||
serving path watches two signals — the per-channel cancel (fired by
|
serving path watches two signals: the per-channel cancel (fired by
|
||||||
`channel_close` via the surface's watch) and the connection
|
`channel_close` via the surface's watch) and the connection
|
||||||
teardown (fired by the handler's `Drop`; the russh session ended
|
teardown (fired by the handler's `Drop`; the russh session ended
|
||||||
however it ended). On either: the serving task **aborts the alktty
|
however it ended). On either: the serving task **aborts the alktty
|
||||||
session task**, dropping its not-yet-resolved `exit_code` future —
|
session task**, dropping its not-yet-resolved `exit_code` future,
|
||||||
alktty's ADR-056 kill-on-drop contract is the cancelled session's
|
letting alktty's ADR-056 kill-on-drop contract clean up the
|
||||||
cleanup — then closes the channel and ends. Witnessed live by the
|
cancelled session, then closes the channel and ends. Witnessed live by the
|
||||||
shim's kill log row.
|
shim's kill log row.
|
||||||
|
|
||||||
- **The contract tests** (`tests/tty_mapping.rs`, session-gated;
|
- **The contract tests** (`tests/tty_mapping.rs`, session-gated;
|
||||||
`tests/common/tty_doubles.rs`): the shim double (hand-rolled per
|
`tests/common/tty_doubles.rs`): the shim double (hand-rolled per
|
||||||
the `TtyHandle` field shapes — alktty's `MockBackend` is
|
the `TtyHandle` field shapes, because alktty's `MockBackend` is
|
||||||
`pub(crate)` and never importable) records TtyParams, consumes
|
`pub(crate)` and never importable) records TtyParams, consumes
|
||||||
stdin/EOF/resizes/signals, emits scripted chunks and exit codes,
|
stdin/EOF/resizes/signals, emits scripted chunks and exit codes,
|
||||||
and carries the kill-on-drop witness; the ownership witness double
|
and carries the kill-on-drop witness; the ownership witness double
|
||||||
@@ -250,20 +252,21 @@ proved, re-landed as contract:
|
|||||||
cap row fixed). `env_request` decline unchanged (OQ-09).
|
cap row fixed). `env_request` decline unchanged (OQ-09).
|
||||||
|
|
||||||
- **The identity ride** (the parked wave-2 item, discharged): asserted
|
- **The identity ride** (the parked wave-2 item, discharged): asserted
|
||||||
through alktty's ADR-050 ownership check — the shim's
|
through alktty's ADR-050 ownership check, with the shim's
|
||||||
`resource_id` guarantees the check evaluates, and the ownership
|
`resource_id` guaranteeing the check evaluates, and the ownership
|
||||||
double records the resolved identity; the deny shape also pins the
|
double records the resolved identity; the deny shape also pins the
|
||||||
negotiation-error wire shape.
|
negotiation-error wire shape.
|
||||||
|
|
||||||
- **Real-tool smoke posture (recorded)**: one OpenSSH `-tt` pty
|
- **Real-tool smoke posture (recorded)**: one OpenSSH `-tt` pty
|
||||||
session (`ssh -tt … smoke-rider@127.0.0.1` with piped stdin) through
|
session (`ssh -tt … smoke-rider@127.0.0.1` with piped stdin) through
|
||||||
a TCP-mounted adapter against a **real PTY via a dev-dep alktty
|
a TCP-mounted adapter against a **real PTY via a dev-dep alktty
|
||||||
`local` composition** — the decided posture: a shim row cannot show
|
`local` composition**, the decided posture: a shim row cannot show
|
||||||
what vanilla OpenSSH really sends, and POC-3 stage D proved exactly
|
what vanilla OpenSSH really sends, and POC-3 stage D proved exactly
|
||||||
this composition. The dev-dep `features = ["local"]` unification
|
this composition. The dev-dep `features = ["local"]` unification
|
||||||
applies to the test graph only (the published lib never enables it;
|
applies to the test graph only (the published lib never enables it;
|
||||||
`src/` never names a local shape). Requires ssh + sh; absent
|
`src/` never names a local shape). Requires ssh + sh; absent
|
||||||
tooling is skipped **with a printed reason** — never silently green.
|
tooling is skipped **with a printed reason**, never silently
|
||||||
|
green.
|
||||||
The smoke pins the pty lifecycle (serve, stdin ride, clean exit →
|
The smoke pins the pty lifecycle (serve, stdin ride, clean exit →
|
||||||
exit-status(0) before close); the `stty size`-grade fidelity matrix
|
exit-status(0) before close); the `stty size`-grade fidelity matrix
|
||||||
is wave 5's.
|
is wave 5's.
|
||||||
@@ -273,4 +276,4 @@ proved, re-landed as contract:
|
|||||||
`cargo test --features tunnel`, `cargo test --all-features`;
|
`cargo test --features tunnel`, `cargo test --all-features`;
|
||||||
`cargo clippy --all-targets -- -D warnings` ×5 (default / session /
|
`cargo clippy --all-targets -- -D warnings` ×5 (default / session /
|
||||||
git / tunnel / all-features); `cargo fmt --check`; `cargo doc
|
git / tunnel / all-features); `cargo fmt --check`; `cargo doc
|
||||||
--no-deps` (default + session + all-features) — all green.
|
--no-deps` (default + session + all-features); all green.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w3-tunnel-direct
|
id: w3-tunnel-direct
|
||||||
name: Wave 3 — tunnel leg 1 (the route-1 bridge — direct-tcpip + streamlocal)
|
name: "Wave 3: tunnel leg 1 (the route-1 bridge, direct-tcpip + streamlocal)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w3-tty-session]
|
depends_on: [w3-tty-session]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -11,71 +11,72 @@ level: implementation
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the tunnel branch's proven leg (ADR-006 decision 1) — the
|
Land the tunnel branch's proven leg (ADR-006 decision 1), placing the
|
||||||
adapter-side route-1 bridge for `direct-tcpip` and `direct-streamlocal`
|
adapter-side route-1 bridge for `direct-tcpip` and `direct-streamlocal`
|
||||||
(POC-3 stage C's shape, re-landed as contract):
|
(POC-3 stage C's shape, re-landed as contract):
|
||||||
|
|
||||||
- **The route-1 bridge** — the `tunnel_row_stub` replaced with the
|
- **The route-1 bridge**, the `tunnel_row_stub` replaced with the
|
||||||
real flow: per accepted open, the target/substrate are constructed
|
real flow: per accepted open, the target/substrate are constructed
|
||||||
from the SSH open arguments (`direct-tcpip`(host, port) →
|
from the SSH open arguments (`direct-tcpip`(host, port) →
|
||||||
`SubstrateAddr::HostPort` + `Substrate::Tcp`; `direct-streamlocal`
|
`SubstrateAddr::HostPort` + `Substrate::Tcp`; `direct-streamlocal`
|
||||||
(socket path) → `SubstrateAddr::Path` + `Substrate::Unix`) and
|
(socket path) → `SubstrateAddr::Path` + `Substrate::Unix`) and
|
||||||
spawn the flow POC-3 proved: **`TunnelSession::open_direct(&
|
spawn the flow POC-3 proved: **`TunnelSession::open_direct(&
|
||||||
channel_client, target, substrate)`** → **accept only after the
|
channel_client, target, substrate)`** → **accept only after the
|
||||||
fabric open succeeds** (accept consumes the handle — the reject
|
fabric open succeeds** (accept consumes the handle, and the
|
||||||
shape is only possible pre-accept) → on fabric failure the explicit
|
reject shape is only possible pre-accept) → on fabric failure the explicit
|
||||||
`ConnectFailed` reject (the stub's reason survives as the real
|
`ConnectFailed` reject (the stub's reason survives as the real
|
||||||
decline; `tunnel_seam_stub_decline` retires into it) → on success
|
decline; `tunnel_seam_stub_decline` retires into it) → on success
|
||||||
`take_halves()` pumped against the taken channel's
|
`take_halves()` pumped against the taken channel's
|
||||||
stream (`pump_bidi` — the alkcall primitive the POC recipe used) on
|
stream (`pump_bidi`, the alkcall primitive the POC recipe used) on
|
||||||
the spawned task, per spawn-and-return. The open hook returns fast
|
the spawned task, per spawn-and-return. The open hook returns fast
|
||||||
with the fabric open in flight.
|
with the fabric open in flight.
|
||||||
- **The cap admission across the fabric window** — the wave-2 stub
|
- **The cap admission across the fabric window**, where the wave-2 stub
|
||||||
declined without consuming a count; the real flow admits. Mechanics
|
declined without consuming a count; the real flow admits. Mechanics
|
||||||
to get right (record): the admission is taken at the hook (the
|
to get right (record): the admission is taken at the hook (the
|
||||||
packet-loop single-writer site — no race window against the cap
|
packet-loop single-writer site, with no race window against the
|
||||||
check) and held across the fabric open; **a fabric failure must
|
cap check) and held across the fabric open; **a fabric failure must
|
||||||
release the count inside the spawned task** (the
|
release the count inside the spawned task** (the
|
||||||
`channel_close`-hook release point only fires for established
|
`channel_close`-hook release point only fires for established
|
||||||
channels — a rejected open never established one), so a failed
|
channels, and a rejected open never established one), so a failed
|
||||||
bridge cannot leak budget. Decide the release mechanism and
|
bridge cannot leak budget. Decide the release mechanism and
|
||||||
record.
|
record.
|
||||||
- **Identity propagation to the family fabric (auth.md §Server
|
- **Identity propagation to the family fabric (auth.md §Server
|
||||||
direction step 4 — the visible-surface invariant's gate)**: the
|
direction step 4, the visible-surface invariant's gate)**: the
|
||||||
resolved identity rides the channels connection so the fabric's
|
resolved identity rides the channels connection so the fabric's
|
||||||
`AccessControl::check(peer_identity)` gates the mapped opens. The
|
`AccessControl::check(peer_identity)` gates the mapped opens. The
|
||||||
parked wave-2 `auth_succeeded` item lands here: on auth completion
|
parked wave-2 `auth_succeeded` item lands here: on auth completion
|
||||||
the handler propagates **once** via alkcall's `Connection::
|
the handler propagates **once** via alkcall's `Connection::
|
||||||
set_identity` — mechanics decided and recorded (the alkhub
|
set_identity`, with mechanics decided and recorded (the alkhub
|
||||||
`hub_leg.rs` one-time-set precedent: `ChannelClient::
|
`hub_leg.rs` one-time-set precedent: `ChannelClient::
|
||||||
take_call_connection()` → `set_identity(identity)`; check the
|
take_call_connection()` → `set_identity(identity)`; check the
|
||||||
`take` semantics against alkcall 0.10.1's docs — the relay note
|
`take` semantics against alkcall 0.10.1's docs, since the relay
|
||||||
|
note
|
||||||
says the detach is wrong for hubs; verify a door's reading and
|
says the detach is wrong for hubs; verify a door's reading and
|
||||||
hook placement, `auth_succeeded` vs the first seam call).
|
hook placement, `auth_succeeded` vs the first seam call).
|
||||||
- **The runtime-Unix gate** (OQ-11 — universal compile, clean
|
- **The runtime-Unix gate** (OQ-11: universal compile, clean
|
||||||
open-decline off-Unix): `direct-streamlocal` compiles wherever
|
open-decline off-Unix): `direct-streamlocal` compiles wherever
|
||||||
`tunnel` compiles; off-Unix a clean open-decline **before any
|
`tunnel` compiles; off-Unix a clean open-decline **before any
|
||||||
fabric call** (never a partial bridge); on-Unix the real bridge.
|
fabric call** (never a partial bridge); on-Unix the real bridge.
|
||||||
- **Contract rows through the harness, per build** (the wave-2 gate's
|
- **Contract rows through the harness, per build** (the wave-2 gate's
|
||||||
booked acceptor vocabulary note is the listen leg's — this task's
|
booked acceptor vocabulary note is the listen leg's, so this
|
||||||
rows ride plain client opens): a git/session build with `tunnel`
|
task's rows ride plain client opens): a git/session build with `tunnel`
|
||||||
off declines the tunnel opens (unfeatured, `AdministrativelyProhibited`);
|
off declines the tunnel opens (unfeatured, `AdministrativelyProhibited`);
|
||||||
the featured rows: a configured `tunnel` handle (feature + `Arc<
|
the featured rows: a configured `tunnel` handle (feature + `Arc<
|
||||||
ChannelClient>`) serves, an **absent handle declined with a clean
|
ChannelClient>`) serves, an **absent handle declined with a clean
|
||||||
error even under the feature** (the unchanged unset-declines
|
error even under the feature** (the unchanged unset-declines
|
||||||
posture — the wave-1 config note), the cap rows (cap-gated before
|
posture, the wave-1 config note), the cap rows (cap-gated before
|
||||||
spawn — never a leaked count over a failed bridge: the release
|
spawn, never a leaked count over a failed bridge, meaning the
|
||||||
mechanism pinning row), and the `ConnectFailed` row over a refusing
|
release mechanism pinning row), and the `ConnectFailed` row over a refusing
|
||||||
fabric double. The **fabric double** is the POC-3 §C.4 in-process
|
fabric double. The **fabric double** is the POC-3 §C.4 in-process
|
||||||
recipe as test composition (ChannelsAdapter + InstallChannelZero +
|
recipe as test composition (ChannelsAdapter + InstallChannelZero +
|
||||||
the alktunnels openables + a hand-rolled dial/dial-fail seam) —
|
the alktunnels openables + a hand-rolled dial/dial-fail seam),
|
||||||
hand-rolled in `tests/`, never a sibling-checkout path.
|
hand-rolled in `tests/`, never a sibling-checkout path.
|
||||||
- **The optional dep lands here** (`alktunnels = "0.4.1"`, wired
|
- **The optional dep lands here** (`alktunnels = "0.4.1"`, wired
|
||||||
`tunnel = ["dep:alktunnels"]` — first consuming code; crates.io pin
|
`tunnel = ["dep:alktunnels"]` (first consuming code; the crates.io
|
||||||
re-verified before committing).
|
pin re-verified before committing).
|
||||||
- **The real-tool smoke** — one `ssh -L` (+ curl) through the
|
- **The real-tool smoke**: one `ssh -L` (+ curl) through the
|
||||||
harness against the fabric double (tooling posture decided like the
|
harness against the fabric double (tooling posture decided like the
|
||||||
git task — required vs conditional-skip, recorded).
|
git task, either required or conditional-skip, recorded).
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
@@ -84,8 +85,8 @@ adapter-side route-1 bridge for `direct-tcpip` and `direct-streamlocal`
|
|||||||
explicit `ConnectFailed` reject, the stub retired; the open
|
explicit `ConnectFailed` reject, the stub retired; the open
|
||||||
hook returns fast
|
hook returns fast
|
||||||
- [x] The cap admission across the fabric window pinned (admit at the
|
- [x] The cap admission across the fabric window pinned (admit at the
|
||||||
hook, release on fabric failure inside the spawned task — no
|
hook, release on fabric failure inside the spawned task, with
|
||||||
leaked count, no double-admit)
|
no leaked count and no double-admit)
|
||||||
- [x] The identity propagates to the channels connection once
|
- [x] The identity propagates to the channels connection once
|
||||||
(`set_identity` mechanics + hook placement decided and
|
(`set_identity` mechanics + hook placement decided and
|
||||||
recorded); the wave-2 `auth_succeeded` item discharged
|
recorded); the wave-2 `auth_succeeded` item discharged
|
||||||
@@ -105,7 +106,7 @@ adapter-side route-1 bridge for `direct-tcpip` and `direct-streamlocal`
|
|||||||
- docs/architecture/mapping.md (the `direct-tcpip` / `direct-
|
- docs/architecture/mapping.md (the `direct-tcpip` / `direct-
|
||||||
streamlocal` channel-type rows; §the structural constraint)
|
streamlocal` channel-type rows; §the structural constraint)
|
||||||
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
||||||
(decision 1 — route 1; the reject shape; the wiring recipe pointer)
|
(decision 1, route 1; the reject shape; the wiring recipe pointer)
|
||||||
- docs/architecture/decisions/003-feature-graph.md (the tunnel row,
|
- docs/architecture/decisions/003-feature-graph.md (the tunnel row,
|
||||||
the OQ-11 runtime-Unix gate)
|
the OQ-11 runtime-Unix gate)
|
||||||
- docs/architecture/auth.md §Server direction step 4 (set_identity,
|
- docs/architecture/auth.md §Server direction step 4 (set_identity,
|
||||||
@@ -126,26 +127,26 @@ adapter-side route-1 bridge for `direct-tcpip` and `direct-streamlocal`
|
|||||||
> the module docs (`src/tunnel_map.rs`, `src/channel_cap.rs`,
|
> the module docs (`src/tunnel_map.rs`, `src/channel_cap.rs`,
|
||||||
> `src/handler.rs`) and are summarized here for the gate.
|
> `src/handler.rs`) and are summarized here for the gate.
|
||||||
|
|
||||||
- **The cap release mechanism (the task's first decide-and-record)** —
|
- **The cap release mechanism (the task's first decide-and-record)**:
|
||||||
`ChannelCap` became a `parking_lot::Mutex<usize>` shared as an
|
`ChannelCap` became a `parking_lot::Mutex<usize>` shared as an
|
||||||
`Arc`: **admissions stay packet-loop-only** (every `admit` call site
|
`Arc`: **admissions stay packet-loop-only** (every `admit` call site
|
||||||
is an open hook — the check-and-increment is one atomic section
|
is an open hook, so the check-and-increment is one atomic section
|
||||||
against the shared writer), while **releases have exactly two
|
against the shared writer), while **releases have exactly two
|
||||||
disjoint sites** — the `channel_close` hook for *established*
|
disjoint sites**, the `channel_close` hook for *established*
|
||||||
channels, and *inside the spawned bridge task* on a fabric-open
|
channels, and *inside the spawned bridge task* on a fabric-open
|
||||||
failure (the admitted-but-rejected shape: ADR-006 decision 1 rejects
|
failure (the admitted-but-rejected shape: ADR-006 decision 1 rejects
|
||||||
the open, so no `CHANNEL_CLOSE` can ever cycle back, and the count
|
the open, so no `CHANNEL_CLOSE` can ever cycle back, and the count
|
||||||
must be released where the failure is known — before the reject is
|
must be released where the failure is known, before the reject is
|
||||||
answered, so the very next open observes the recovered budget).
|
answered, so the very next open observes the recovered budget).
|
||||||
Both release sites saturate. Pinning rows:
|
Both release sites saturate. Pinning rows:
|
||||||
`a_failed_bridge_releases_the_cap_count_before_the_next_open`
|
`a_failed_bridge_releases_the_cap_count_before_the_next_open`
|
||||||
(failure-then-serve under `max_concurrent_channels: 1` — a leaked
|
(failure-then-serve under `max_concurrent_channels: 1`, where a leaked
|
||||||
count would answer `AdministrativelyProhibited`) and
|
count would answer `AdministrativelyProhibited`) and
|
||||||
`the_cap_refuses_beyond_the_ceiling_and_recovers_on_close`
|
`the_cap_refuses_beyond_the_ceiling_and_recovers_on_close`
|
||||||
(refusal + close-hook recovery).
|
(refusal + close-hook recovery).
|
||||||
|
|
||||||
- **`set_identity` mechanics (the task's second decide-and-record; the
|
- **`set_identity` mechanics (the task's second decide-and-record; the
|
||||||
`w2-auth-bridge` parked item discharged)** — the alkhub `hub_leg.rs`
|
`w2-auth-bridge` parked item discharged)**, the alkhub `hub_leg.rs`
|
||||||
one-time-set precedent is a **serving-side** set (the hub resolves
|
one-time-set precedent is a **serving-side** set (the hub resolves
|
||||||
the peer identity and sets it on its accept-side channel-0
|
the peer identity and sets it on its accept-side channel-0
|
||||||
`Connection` inside the channel-0 install, before any dispatch).
|
`Connection` inside the channel-0 install, before any dispatch).
|
||||||
@@ -154,45 +155,46 @@ adapter-side route-1 bridge for `direct-tcpip` and `direct-streamlocal`
|
|||||||
`CallConnection::connection()` → `Connection::set_identity`) was
|
`CallConnection::connection()` → `Connection::set_identity`) was
|
||||||
**checked and rejected for this door**, verified against alkcall
|
**checked and rejected for this door**, verified against alkcall
|
||||||
0.10.1's docs: the take is a one-shot single-slot detach
|
0.10.1's docs: the take is a one-shot single-slot detach
|
||||||
("taking it detaches the client's own calling surface" — every later
|
("taking it detaches the client's own calling surface", so every
|
||||||
|
later
|
||||||
`call_open_op`, and with it every `TunnelSession::open_direct`,
|
`call_open_op`, and with it every `TunnelSession::open_direct`,
|
||||||
errors "channel client closed"), and the slot is the **shared
|
errors "channel client closed"), and the slot is the **shared
|
||||||
assembly handle** (`SshAdapter` hands one `Arc<ServeConfig>` to
|
assembly handle** (`SshAdapter` hands one `Arc<ServeConfig>` to
|
||||||
every mounted session), so the first session's take would detach
|
every mounted session), so the first session's take would detach
|
||||||
the open surface for every later session of the whole serving
|
the open surface for every later session of the whole serving
|
||||||
direction — the relay note's "the detach is wrong for hubs"
|
direction, and the relay note's "the detach is wrong for hubs"
|
||||||
applies a fortiori. The identity that alkcall's protocol consults
|
applies a fortiori. The identity that alkcall's protocol consults
|
||||||
at the fabric's serving dispatch is the channels connection's
|
at the fabric's serving dispatch is the channels connection's
|
||||||
**dial identity** (CF-005: set once at dial, propagated to channel
|
**dial identity** (CF-005: set once at dial, propagated to channel
|
||||||
0 at `from_connection`) — identity crossing the alkcall wire is not
|
0 at `from_connection`), since identity crossing the alkcall wire
|
||||||
a protocol concept. Decision: the composition — which identity the
|
is not a protocol concept. Decision: the composition, meaning which identity the
|
||||||
fabric leg dials with, against which the fabric's ACL gates
|
fabric leg dials with, against which the fabric's ACL gates
|
||||||
alkssh's mapped opens — is **assembly policy** (the assembly
|
alkssh's mapped opens, is **assembly policy** (the assembly
|
||||||
supplies both the injected `IdentityProvider` and the fabric leg);
|
supplies both the injected `IdentityProvider` and the fabric leg);
|
||||||
alkssh structurally refuses anonymous serving (the `no-identity`
|
alkssh structurally refuses anonymous serving (the `no-identity`
|
||||||
open guard, unreachable on the wire — russh authorizes before
|
open guard, unreachable on the wire since russh authorizes before
|
||||||
channels exist — fail-closed like the git/tty rows) so no
|
channels exist, fail-closed like the git/tty rows) so no
|
||||||
SSH-shaped exception to the visible-surface invariant exists, and
|
SSH-shaped exception to the visible-surface invariant exists, and
|
||||||
the harness rows witness the composed gate end to end:
|
the harness rows witness the composed gate end to end:
|
||||||
`the_fabric_acl_refuses_an_unscoped_channels_identity` (the
|
`the_fabric_acl_refuses_an_unscoped_channels_identity` (the
|
||||||
fabric's `AccessControl::check` refusing a scopeless channels
|
fabric's `AccessControl::check` refusing a scopeless channels
|
||||||
identity — the establisher never dialed) and
|
identity, the establisher never dialed) and
|
||||||
`direct_tcpip_serves_through_the_route_1_bridge`'s identity-witness
|
`direct_tcpip_serves_through_the_route_1_bridge`'s identity-witness
|
||||||
assertion (the establisher's per-call auth seeing the channels
|
assertion (the establisher's per-call auth seeing the channels
|
||||||
identity — POC-3 §C.3). **Hook placement**: the identity store
|
identity, POC-3 §C.3). **Hook placement**: the identity store
|
||||||
stays the `auth_publickey` ride-along in handler state; the
|
stays the `auth_publickey` ride-along in handler state; the
|
||||||
tunnel rows' dispatch reads it; `auth_succeeded` stays
|
tunnel rows' dispatch reads it; `auth_succeeded` stays
|
||||||
unimplemented (russh default) — nothing rides it that the store
|
unimplemented (russh default), since nothing rides it that the
|
||||||
does not carry. **Upstream ask booked for the wave-3 gate**: a
|
store does not carry. **Upstream ask booked for the wave-3 gate**: a
|
||||||
non-detaching post-construction identity seam on `ChannelClient`
|
non-detaching post-construction identity seam on `ChannelClient`
|
||||||
(or per-session fabric legs) would let the door attach per-peer
|
(or per-session fabric legs) would let the door attach per-peer
|
||||||
identities to a shared leg; nothing is silently absorbed.
|
identities to a shared leg; nothing is silently absorbed.
|
||||||
|
|
||||||
- **Wire decline classes (recorded)** — two vanilla-observable classes
|
- **Wire decline classes (recorded)**, two vanilla-observable
|
||||||
at the open: *policy refusals* → `AdministrativelyProhibited`
|
classes at the open: *policy refusals* → `AdministrativelyProhibited`
|
||||||
(unfeatured, cap-exceeded, the `no-identity` guard) vs
|
(unfeatured, cap-exceeded, the `no-identity` guard) vs
|
||||||
*cannot-bridge* → `ConnectFailed`
|
*cannot-bridge* → `ConnectFailed`
|
||||||
([`dispatch::connect_failed_open_decline`] — the wave-2 stub's
|
([`dispatch::connect_failed_open_decline`], the wave-2 stub's
|
||||||
reason surviving as the real decline): the failed fabric open, the
|
reason surviving as the real decline): the failed fabric open, the
|
||||||
absent fabric handle under the feature (`seam-unconfigured`, the
|
absent fabric handle under the feature (`seam-unconfigured`, the
|
||||||
unset-declines posture), an impossible wire target
|
unset-declines posture), an impossible wire target
|
||||||
@@ -201,30 +203,32 @@ adapter-side route-1 bridge for `direct-tcpip` and `direct-streamlocal`
|
|||||||
are distinguishable in the event vocabulary, which gained
|
are distinguishable in the event vocabulary, which gained
|
||||||
`invalid-target` and `unix-unavailable`.
|
`invalid-target` and `unix-unavailable`.
|
||||||
|
|
||||||
- **Admission mechanics** — the pre-admit decline classes are checked
|
- **Admission mechanics**, so the pre-admit decline classes are
|
||||||
|
checked
|
||||||
in order (unset handle → no-identity → invalid target), so every
|
in order (unset handle → no-identity → invalid target), so every
|
||||||
pure-decline path consumes nothing; the admission is taken at the
|
pure-decline path consumes nothing; the admission is taken at the
|
||||||
hook and held across the fabric open; the success release rides the
|
hook and held across the fabric open; the success release rides the
|
||||||
established channel's close. `forwarded-tcpip` kept its
|
established channel's close. `forwarded-tcpip` kept its
|
||||||
pre-admit `has_room` cap gate and stub — its `-R` data-leg flow is
|
pre-admit `has_room` cap gate and stub, as its `-R` data-leg flow
|
||||||
`w3-tunnel-listen` material (the harness's russh client cannot
|
is `w3-tunnel-listen` material (the harness's russh client cannot
|
||||||
initiate the type; its wire rows are acceptor vocabulary).
|
initiate the type; its wire rows are acceptor vocabulary).
|
||||||
|
|
||||||
- **The fabric double** — the POC-3 §C.4 in-process recipe as test
|
- **The fabric double**, the POC-3 §C.4 in-process recipe as test
|
||||||
composition (`wire_fabric` in `tests/tunnel_mapping.rs`):
|
composition (`wire_fabric` in `tests/tunnel_mapping.rs`):
|
||||||
`ChannelsAdapter` + `InstallChannelZero` + `ChannelOperations` +
|
`ChannelsAdapter` + `InstallChannelZero` + `ChannelOperations` +
|
||||||
`register_tunnel_direct_openable` + `Dispatcher::serve_single_stream`
|
`register_tunnel_direct_openable` + `Dispatcher::serve_single_stream`
|
||||||
over a tokio duplex, the connect side the `Arc<ChannelClient>` the
|
over a tokio duplex, the connect side the `Arc<ChannelClient>` the
|
||||||
serve-config holds. The hand-rolled dial/dial-fail seam is an
|
serve-config holds. The hand-rolled dial/dial-fail seam is an
|
||||||
in-process map of duplex halves keyed by the establisher-rendered
|
in-process map of duplex halves keyed by the establisher-rendered
|
||||||
target string (consume-once — the cap-recovery row reads the
|
target string (consume-once, and the cap-recovery row reads the
|
||||||
consume-once dial as its release signal), with an atomics call
|
consume-once dial as its release signal), with an atomics call
|
||||||
counter for the "never called" assertions (the ACL row and the
|
counter for the "never called" assertions (the ACL row and the
|
||||||
OQ-11 row); the smoke's dial seam is a real-TCP closure (alktunnels'
|
OQ-11 row); the smoke's dial seam is a real-TCP closure (alktunnels'
|
||||||
`local` feature stays off everywhere — the shipped crate and the
|
`local` feature stays off everywhere, in both the shipped crate
|
||||||
tests).
|
and the tests).
|
||||||
|
|
||||||
- **The real-tool smoke** — tooling posture decided like the git task
|
- **The real-tool smoke**, with tooling posture decided like the git
|
||||||
|
task
|
||||||
(conditional skip with a printed reason when `ssh`/`curl` is
|
(conditional skip with a printed reason when `ssh`/`curl` is
|
||||||
absent), and it ran green here: one `ssh -N -L` (OpenSSH client)
|
absent), and it ran green here: one `ssh -N -L` (OpenSSH client)
|
||||||
through a TCP-mounted adapter → the route-1 bridge → the fabric
|
through a TCP-mounted adapter → the route-1 bridge → the fabric
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w3-tunnel-listen
|
id: w3-tunnel-listen
|
||||||
name: Wave 3 — tunnel leg 2 (the -R listen flow + the forwarded-tcpip data leg)
|
name: "Wave 3: tunnel leg 2 (the -R listen flow + the forwarded-tcpip data leg)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w3-tunnel-direct]
|
depends_on: [w3-tunnel-direct]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -12,18 +12,18 @@ level: implementation
|
|||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the `-R` leg's server direction (mapping.md §Global-request rows +
|
Land the `-R` leg's server direction (mapping.md §Global-request rows +
|
||||||
the `forwarded-tcpip` channel-type row) — the one leg the POC only
|
the `forwarded-tcpip` channel-type row), and it is the one leg the POC only
|
||||||
exercised as refusal ("only the refusal shape was exercised"), so this
|
exercised as refusal ("only the refusal shape was exercised"), so this
|
||||||
task carries the wave-3 gate's biggest open mechanics. Shape per the
|
task carries the wave-3 gate's biggest open mechanics. Shape per the
|
||||||
family door semantics (vanilla -R: the door's fabric listens, per
|
family door semantics (vanilla -R: the door's fabric listens, per
|
||||||
arrival the data flows back to the vanilla client, which dials its own
|
arrival the data flows back to the vanilla client, which dials its own
|
||||||
target):
|
target):
|
||||||
|
|
||||||
- **The listen request rows** — `tcpip_forward` / `streamlocal_forward`
|
- **The listen request rows**: `tcpip_forward` / `streamlocal_forward`
|
||||||
(and their cancel forms) go from wave-2's unconditional decline to
|
(and their cancel forms) go from wave-2's unconditional decline to
|
||||||
the real flow under `tunnel` (+ the streamlocal runtime-Unix gate
|
the real flow under `tunnel` (+ the streamlocal runtime-Unix gate
|
||||||
riding its serving flow, OQ-11): the listen is mapped **family-ward**
|
riding its serving flow, OQ-11): the listen is mapped **family-ward**
|
||||||
through the alktunnels listen/forwarded shapes — the door owns
|
through the alktunnels listen/forwarded shapes, and the door owns
|
||||||
nothing local. The **exact composition is decided here** against
|
nothing local. The **exact composition is decided here** against
|
||||||
alktunnels 0.4.1's actual producer/consumer surfaces (`TunnelListener::
|
alktunnels 0.4.1's actual producer/consumer surfaces (`TunnelListener::
|
||||||
open`'s listen dial + reply fields; the minted listen token in
|
open`'s listen dial + reply fields; the minted listen token in
|
||||||
@@ -36,52 +36,52 @@ target):
|
|||||||
door-side acceptance of the fabric's inbound forwarded opens
|
door-side acceptance of the fabric's inbound forwarded opens
|
||||||
(alkcall channel-manager openable registration on the door-side
|
(alkcall channel-manager openable registration on the door-side
|
||||||
connection) or the serve-config's tunnel handle must grow a
|
connection) or the serve-config's tunnel handle must grow a
|
||||||
registration-seam handle — **if the door-side handle shape must be
|
registration-seam handle; **if the door-side handle shape must be
|
||||||
amended, amend it here** (the `w3-seam-config` shape, recorded as
|
amended, amend it here** (the `w3-seam-config` shape, recorded as
|
||||||
this wave's second config course-correction). If the topology does
|
this wave's second config course-correction). If the topology does
|
||||||
not close over the published alkcall 0.10.1 / alktunnels 0.4.1
|
not close over the published alkcall 0.10.1 / alktunnels 0.4.1
|
||||||
surfaces, **stop and book it** for the gate — never silently
|
surfaces, **stop and book it** for the gate, never silently
|
||||||
forked, worked around in-family, or re-litigating ADR-006's
|
forked, worked around in-family, or re-litigating ADR-006's
|
||||||
resolution.
|
resolution.
|
||||||
- **The `forwarded-tcpip` data leg** (mapping.md's row — "hand the
|
- **The `forwarded-tcpip` data leg** (mapping.md's row, "hand the
|
||||||
channel duplex to the handler"): per fabric arrival, the door opens
|
channel duplex to the handler"): per fabric arrival, the door opens
|
||||||
a `forwarded-tcpip` channel **toward the vanilla client** with the
|
a `forwarded-tcpip` channel **toward the vanilla client** with the
|
||||||
SSH message's fields mapped per the vanilla -R convention
|
SSH message's fields mapped per the vanilla -R convention
|
||||||
(`host_to_connect`/`port` echoing the target args the original
|
(`host_to_connect`/`port` echoing the target args the original
|
||||||
`tcpip_forward` request carried — recorded per pending listen;
|
`tcpip_forward` request carried, recorded per pending listen;
|
||||||
originator fields carrying the arrival info — decide and record
|
originator fields carrying the arrival info, decide and record
|
||||||
what `TunnelForwardedParams.peer` yields) → accept-or-reject per
|
what `TunnelForwardedParams.peer` yields) → accept-or-reject per
|
||||||
the in-crate cap (the **acceptor vocabulary** the wave-2 gate
|
the in-crate cap (the **acceptor vocabulary** the wave-2 gate
|
||||||
booked — the first shape of what wave 4 grows into the client-side
|
booked, the first shape of what wave 4 grows into the client-side
|
||||||
acceptor registry) → pump the fabric `BiStream` ↔ the SSH channel
|
acceptor registry) → pump the fabric `BiStream` ↔ the SSH channel
|
||||||
(`ForwardedHandler` ownership: the handler owns the pump per
|
(`ForwardedHandler` ownership: the handler owns the pump per
|
||||||
alktunnels' shape; correlation-miss → teardown-at-birth, never a
|
alktunnels' shape; correlation-miss → teardown-at-birth, never a
|
||||||
half-open SSH channel).
|
half-open SSH channel).
|
||||||
- **Cancel forms + bounded bookkeeping** — a per-session listener map
|
- **Cancel forms + bounded bookkeeping**, with a per-session listener map
|
||||||
(the bound-port handles) drives `cancel_tcpip_forward` /
|
(the bound-port handles) drives `cancel_tcpip_forward` /
|
||||||
`cancel_streamlocal_forward` 1:1 into the family listener's
|
`cancel_streamlocal_forward` 1:1 into the family listener's
|
||||||
teardown (`TunnelListener::close` posture); the listener count is a
|
teardown (`TunnelListener::close` posture); the listener count is a
|
||||||
bounded resource (decide the bound relation to
|
bounded resource (decide the bound relation to
|
||||||
`Limits::max_concurrent_channels` or a dedicated cap — record; no
|
`Limits::max_concurrent_channels` or a dedicated cap, recorded; no
|
||||||
unbounded map). The bound-port out-param: `port = 0` → the
|
unbounded map). The bound-port out-param: `port = 0` → the
|
||||||
actually-bound number returned through the `&mut u32` (the
|
actually-bound number returned through the `&mut u32` (the
|
||||||
family-side listen reply's numbers; the mapping.md/client.md
|
family-side listen reply's numbers; the mapping.md/client.md
|
||||||
lifecycle spec).
|
lifecycle spec).
|
||||||
- **Identity**: the listen dial rides the propagated identity (the
|
- **Identity**: the listen dial rides the propagated identity (the
|
||||||
fabric's `AccessControl` gates the listen + forwarded opens the
|
fabric's `AccessControl` gates the listen + forwarded opens the
|
||||||
same as leg 1's — the visible-surface invariant across the whole
|
same as leg 1's, giving the visible-surface invariant across the
|
||||||
listen flow).
|
whole listen flow).
|
||||||
- **Contract rows through the harness, per build** — the wave-2 stub
|
- **Contract rows through the harness, per build**, with the wave-2
|
||||||
rows replaced: featured + configured serve (the listen flow against
|
stub rows replaced: featured + configured serve (the listen flow against
|
||||||
the fabric double); unfeatured/unset/streamlocal-off-Unix decline
|
the fabric double); unfeatured/unset/streamlocal-off-Unix decline
|
||||||
cleanly (honest advertisement: a declined listen never left a half
|
cleanly (honest advertisement: a declined listen never left a half
|
||||||
state); the cancel rows; the arrival → forwarded-tcpip → pump row;
|
state); the cancel rows; the arrival → forwarded-tcpip → pump row;
|
||||||
cap rows at the accept point. The **harness grows the client-side
|
cap rows at the accept point. The **harness grows the client-side
|
||||||
accept hook** (russh client
|
accept hook** (russh client
|
||||||
`server_channel_open_forwarded_tcpip` — the server-initiated opens
|
`server_channel_open_forwarded_tcpip`, the server-initiated opens
|
||||||
the contract rows observe; the wave-2 note said exactly this).
|
the contract rows observe; the wave-2 note said exactly this).
|
||||||
- **The real-tool smoke** posture decided + recorded (a real `ssh -R`
|
- **The real-tool smoke** posture decided + recorded (a real `ssh -R`
|
||||||
through the harness's TCP loopback if tooling permits — else the
|
through the harness's TCP loopback if tooling permits; else the
|
||||||
harness rows carry wave 3 and the real-tool `-R` smoke is named
|
harness rows carry wave 3 and the real-tool `-R` smoke is named
|
||||||
wave-5 e2e territory explicitly; never silently dropped).
|
wave-5 e2e territory explicitly; never silently dropped).
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ target):
|
|||||||
- [x] The family-ward listen composition decided against alktunnels
|
- [x] The family-ward listen composition decided against alktunnels
|
||||||
0.4.1's actual surfaces and recorded; the door-side inbound-open
|
0.4.1's actual surfaces and recorded; the door-side inbound-open
|
||||||
mechanics closed (or a serve-config amendment landed + recorded;
|
mechanics closed (or a serve-config amendment landed + recorded;
|
||||||
or stopped-and-booked for the gate — nothing silent)
|
or stopped-and-booked for the gate, nothing silent)
|
||||||
- [x] The `forwarded-tcpip` data leg serves per the handler shape
|
- [x] The `forwarded-tcpip` data leg serves per the handler shape
|
||||||
(target-arg echo per pending listen, cap-gated acceptor, the
|
(target-arg echo per pending listen, cap-gated acceptor, the
|
||||||
pump, teardown-at-birth on correlation-miss)
|
pump, teardown-at-birth on correlation-miss)
|
||||||
@@ -112,15 +112,15 @@ target):
|
|||||||
- docs/architecture/mapping.md §Channel-type rows (`forwarded-tcpip`
|
- docs/architecture/mapping.md §Channel-type rows (`forwarded-tcpip`
|
||||||
row), §Global-request rows (the `-R` listen flow, the POC-3 note)
|
row), §Global-request rows (the `-R` listen flow, the POC-3 note)
|
||||||
- docs/architecture/client.md §Op rows (the `-R` leg, the acceptor
|
- docs/architecture/client.md §Op rows (the `-R` leg, the acceptor
|
||||||
framing — the client mirror this leg shares vocabulary with),
|
framing, the client mirror this leg shares vocabulary with),
|
||||||
§Lifecycle primitives (the bound-port spec)
|
§Lifecycle primitives (the bound-port spec)
|
||||||
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
||||||
(decision 1 — the fabric wiring recipe), docs/architecture/
|
(decision 1, the fabric wiring recipe), docs/architecture/
|
||||||
decisions/003-feature-graph.md (the tunnel row + §4 the
|
decisions/003-feature-graph.md (the tunnel row + §4 the
|
||||||
client-side acceptor symmetry)
|
client-side acceptor symmetry)
|
||||||
- docs/research/poc-3-mapping-bakeoff.md §Stage C (the recipe;
|
- docs/research/poc-3-mapping-bakeoff.md §Stage C (the recipe;
|
||||||
§C.2's refusal shape this task goes past), OQ seed 4
|
§C.2's refusal shape this task goes past), OQ seed 4
|
||||||
- docs/research/reverse-runner.md (the `-R` row — the client-side
|
- docs/research/reverse-runner.md (the `-R` row, the client-side
|
||||||
mirror of the same wire vocabulary)
|
mirror of the same wire vocabulary)
|
||||||
- alktunnels 0.4.1: `producer.rs:718-772` (`ForwardedOpener`,
|
- alktunnels 0.4.1: `producer.rs:718-772` (`ForwardedOpener`,
|
||||||
`make_bindfirst_accept_handler`), `producer.rs:982-1082`
|
`make_bindfirst_accept_handler`), `producer.rs:982-1082`
|
||||||
@@ -142,7 +142,7 @@ smoke. 18 server-direction rows + 2 real-tool smokes green in
|
|||||||
|
|
||||||
- **The composition decisions (recorded in `src/tunnel_map.rs`'s
|
- **The composition decisions (recorded in `src/tunnel_map.rs`'s
|
||||||
module doc + `mapping.md` §Global-request rows):**
|
module doc + `mapping.md` §Global-request rows):**
|
||||||
- *Who dials `tunnel/forwarded`*: the fabric (not the door) —
|
- *Who dials `tunnel/forwarded`*: the fabric (not the door);
|
||||||
alktunnels' bind-first accept dispatcher
|
alktunnels' bind-first accept dispatcher
|
||||||
(`make_bindfirst_accept_handler` + the `ForwardedOpener` closure)
|
(`make_bindfirst_accept_handler` + the `ForwardedOpener` closure)
|
||||||
dials the op on the channel-0 call plane toward the listen opener
|
dials the op on the channel-0 call plane toward the listen opener
|
||||||
@@ -151,10 +151,10 @@ smoke. 18 server-direction rows + 2 real-tool smokes green in
|
|||||||
directions collision-free: the door allocates odd as the connect
|
directions collision-free: the door allocates odd as the connect
|
||||||
side, the fabric even as the accept side).
|
side, the fabric even as the accept side).
|
||||||
- *The door-side inbound-open mechanics*: closed over the published
|
- *The door-side inbound-open mechanics*: closed over the published
|
||||||
surfaces — the `Arc<ChannelClient>` alone cannot carry them (a
|
surfaces, as the `Arc<ChannelClient>` alone cannot carry them (a
|
||||||
client serves inbound ops on its channel-0 loop only when built
|
client serves inbound ops on its channel-0 loop only when built
|
||||||
with `from_connection_with_serving` against an `OperationRegistry`
|
with `from_connection_with_serving` against an `OperationRegistry`
|
||||||
the assembly owns; no post-construction way in) — so the
|
the assembly owns; no post-construction way in), so the
|
||||||
serve-config's tunnel handle gained `tunnel_listen_seam`
|
serve-config's tunnel handle gained `tunnel_listen_seam`
|
||||||
(`TunnelListenSeam`, the wave-3 seam-config's second
|
(`TunnelListenSeam`, the wave-3 seam-config's second
|
||||||
course-correction): the same `Arc<OperationRegistry>` the assembly
|
course-correction): the same `Arc<OperationRegistry>` the assembly
|
||||||
@@ -162,7 +162,7 @@ smoke. 18 server-direction rows + 2 real-tool smokes green in
|
|||||||
forwarded openable itself (`register_tunnel_forwarded_openable`
|
forwarded openable itself (`register_tunnel_forwarded_openable`
|
||||||
over a `ChannelCore` wrapping a clone of the held client's
|
over a `ChannelCore` wrapping a clone of the held client's
|
||||||
manager, alkcall's secure default per-identity policy) at
|
manager, alkcall's secure default per-identity policy) at
|
||||||
`SshAdapter::new` — a registration failure is a failed
|
`SshAdapter::new`, where a registration failure is a failed
|
||||||
construction (`AdapterError::ListenRegistration`), never a
|
construction (`AdapterError::ListenRegistration`), never a
|
||||||
half-configured adapter. A scratch probe (published crates only,
|
half-configured adapter. A scratch probe (published crates only,
|
||||||
no `alktunnels/local`) proved the topology end to end before any
|
no `alktunnels/local`) proved the topology end to end before any
|
||||||
@@ -173,10 +173,10 @@ smoke. 18 server-direction rows + 2 real-tool smokes green in
|
|||||||
- *The resource-naming convention (the flow's one open surface)*:
|
- *The resource-naming convention (the flow's one open surface)*:
|
||||||
the vanilla wire bind endpoint rides family-ward as the resource
|
the vanilla wire bind endpoint rides family-ward as the resource
|
||||||
string verbatim (`"localhost:9200"` / `"0.0.0.0:9200"` / the
|
string verbatim (`"localhost:9200"` / `"0.0.0.0:9200"` / the
|
||||||
socket path) — the OpenSSH client normalizes the no-address case
|
socket path); the OpenSSH client normalizes the no-address case
|
||||||
to `"localhost"` before sending (sshd `channel_rfwd_bind_host`
|
to `"localhost"` before sending (sshd `channel_rfwd_bind_host`
|
||||||
parity), the door passes through, and the fabric's
|
parity), the door passes through, and the fabric's
|
||||||
establisher/bind closure (assembly policy — the dynamic-address
|
establisher/bind closure (assembly policy, the dynamic-address
|
||||||
producer opt-in the door's convention fronts) treats it as the
|
producer opt-in the door's convention fronts) treats it as the
|
||||||
bind endpoint. ADR-001's "resource is not an address" discipline
|
bind endpoint. ADR-001's "resource is not an address" discipline
|
||||||
stays a producer-side naming rule; this is a recorded door-to-
|
stays a producer-side naming rule; this is a recorded door-to-
|
||||||
@@ -185,34 +185,34 @@ smoke. 18 server-direction rows + 2 real-tool smokes green in
|
|||||||
as refusal): per arrival → token correlation through the shared
|
as refusal): per arrival → token correlation through the shared
|
||||||
`ListenRouter` (one route serves unboundedly many arrivals;
|
`ListenRouter` (one route serves unboundedly many arrivals;
|
||||||
removed only at the cancel forms/session teardown) → the in-crate
|
removed only at the cancel forms/session teardown) → the in-crate
|
||||||
per-session channel cap admits at the accept surface — the acceptor
|
per-session channel cap admits at the accept surface, the acceptor
|
||||||
vocabulary's first shape, the wave-2 gate's booked item now
|
vocabulary's first shape, the wave-2 gate's booked item now
|
||||||
discharged (never-established arrival paths release inside the
|
discharged (never-established arrival paths release inside the
|
||||||
arrival pump; established closes release through the packet loop's
|
arrival pump; established closes release through the packet loop's
|
||||||
`channel_close` as ever) → `Handle::channel_open_forwarded_tcpip`
|
`channel_close` as ever) → `Handle::channel_open_forwarded_tcpip`
|
||||||
echoing the listen request's own args: the wire address string
|
echoing the listen request's own args: the wire address string
|
||||||
verbatim + the actually-bound port (sshd `c->path` +
|
verbatim + the actually-bound port (sshd `c->path` +
|
||||||
`get_local_port` parity — the OpenSSH client matches that exact
|
`get_local_port` parity, since the OpenSSH client matches that
|
||||||
string + port and dials its own target), the
|
exact string + port and dials its own target), the
|
||||||
`TunnelForwardedParams.peer` arrival mapped to the originator
|
`TunnelForwardedParams.peer` arrival mapped to the originator
|
||||||
fields (HostPort → address/port; Path → path + 0,
|
fields (HostPort → address/port; Path → path + 0,
|
||||||
degenerate-but-typed) → `pump_bidi(ssh, fabric)` (half-close
|
degenerate-but-typed) → `pump_bidi(ssh, fabric)` (half-close
|
||||||
semantics: EOF-shutdown in the completed direction; the vanilla
|
semantics: EOF-shutdown in the completed direction; the vanilla
|
||||||
client closes its half when its target socket dies — the row's
|
client closes its half when its target socket dies (the row's
|
||||||
stand-in — and the cap release follows through the normal close).
|
stand-in), and the cap release follows through the normal close).
|
||||||
Correlation-miss → teardown-at-birth (drop the fabric half; the
|
Correlation-miss → teardown-at-birth (drop the fabric half; the
|
||||||
open op's channel reaps — never a half-open SSH channel). Unix
|
open op's channel reaps, never a half-open SSH channel). Unix
|
||||||
arrivals (`streamlocal_forward` mints) open
|
arrivals (`streamlocal_forward` mints) open
|
||||||
`forwarded-streamlocal@openssh.com` with the path echoed (russh's
|
`forwarded-streamlocal@openssh.com` with the path echoed (russh's
|
||||||
server `Handle` carries that channel-open at this pin; the client
|
server `Handle` carries that channel-open at this pin; the client
|
||||||
hook exists) — the "no russh hook" concern in the wave-2 stub notes
|
hook exists); the "no russh hook" concern in the wave-2 stub notes
|
||||||
was about the door's *inbound* handlers of that type, not the
|
was about the door's *inbound* handlers of that type, not the
|
||||||
server-initiated direction.
|
server-initiated direction.
|
||||||
- **Bounded bookkeeping** (convention 9): the per-session listener
|
- **Bounded bookkeeping** (convention 9): the per-session listener
|
||||||
map is packet-loop-only (like the `channels`/`ptys` stores); its
|
map is packet-loop-only (like the `channels`/`ptys` stores); its
|
||||||
bound is a dedicated counter against the same
|
bound is a dedicated counter against the same
|
||||||
`max_concurrent_channels` ceiling — a listen is at least one future
|
`max_concurrent_channels` ceiling, since a listen is at least one
|
||||||
channel's potential; the listener/channels mix stays unconstrained
|
future channel's potential; the listener/channels mix stays unconstrained
|
||||||
while both stay bounded (no unbound listener map). The route map is
|
while both stay bounded (no unbound listener map). The route map is
|
||||||
bounded by construction: entries minted exactly at accepted listens,
|
bounded by construction: entries minted exactly at accepted listens,
|
||||||
removed 1:1 at the cancels and at the owning session's teardown (the
|
removed 1:1 at the cancels and at the owning session's teardown (the
|
||||||
@@ -220,8 +220,8 @@ smoke. 18 server-direction rows + 2 real-tool smokes green in
|
|||||||
failed open → teardown-at-birth. `cancel` of an unknown
|
failed open → teardown-at-birth. `cancel` of an unknown
|
||||||
(endpoint, bound-port) pair answers `false` (sshd
|
(endpoint, bound-port) pair answers `false` (sshd
|
||||||
cancel-of-unknown parity). Admissions have two sites and releases
|
cancel-of-unknown parity). Admissions have two sites and releases
|
||||||
three since this task — the cap's recorded single-writer reasoning
|
three since this task, and the cap's recorded single-writer
|
||||||
updated in `channel_cap.rs` (mutex-guarded, saturating, both).
|
reasoning updated in `channel_cap.rs` (mutex-guarded, saturating, both).
|
||||||
- **The real-tool smoke**: implemented and green here (tooling
|
- **The real-tool smoke**: implemented and green here (tooling
|
||||||
check-gated like the -L smoke's posture): a real `ssh -R
|
check-gated like the -L smoke's posture): a real `ssh -R
|
||||||
127.0.0.1:PORT:HTTP` (OpenSSH client) through the TCP-mounted
|
127.0.0.1:PORT:HTTP` (OpenSSH client) through the TCP-mounted
|
||||||
@@ -231,8 +231,8 @@ smoke. 18 server-direction rows + 2 real-tool smokes green in
|
|||||||
- **The decline matrix** per build, all through the harness with the
|
- **The decline matrix** per build, all through the harness with the
|
||||||
client-side accept hook in place (the harness handler's
|
client-side accept hook in place (the harness handler's
|
||||||
`server_channel_open_forwarded_tcpip` /
|
`server_channel_open_forwarded_tcpip` /
|
||||||
`server_channel_open_forwarded_streamlocal`, acceptor-driven — no
|
`server_channel_open_forwarded_streamlocal`, acceptor-driven, so
|
||||||
acceptor → clean decline, the family's client-side contract): the
|
no acceptor means a clean decline, the family's client-side contract): the
|
||||||
wave-2 stub listen rows migrated (`tcpip_forward_decline_emits_*`
|
wave-2 stub listen rows migrated (`tcpip_forward_decline_emits_*`
|
||||||
split per build: `unfeatured` vs `seam-unconfigured`); the
|
split per build: `unfeatured` vs `seam-unconfigured`); the
|
||||||
featured + configured rows serve/arrive/cancel (incl. the port-0
|
featured + configured rows serve/arrive/cancel (incl. the port-0
|
||||||
@@ -243,11 +243,11 @@ smoke. 18 server-direction rows + 2 real-tool smokes green in
|
|||||||
rows (the fabric-scope gate on the listen dial, the door-side ACL
|
rows (the fabric-scope gate on the listen dial, the door-side ACL
|
||||||
gate on arrivals); the OQ-11 off-Unix rows; the correlation-miss
|
gate on arrivals); the OQ-11 off-Unix rows; the correlation-miss
|
||||||
row; the unvanilla inbound `forwarded-tcpip` open declined
|
row; the unvanilla inbound `forwarded-tcpip` open declined
|
||||||
(event-level contract — russh's client has no arbitrary-type open
|
(event-level contract, as russh's client has no arbitrary-type
|
||||||
to drive that row on the wire).
|
open to drive that row on the wire).
|
||||||
- **`tests/common` grew**: the forwarding acceptor
|
- **`tests/common` grew**: the forwarding acceptor
|
||||||
(`ForwardedAcceptor` + `ForwardedOpen` + `drive_with_acceptor`)
|
(`ForwardedAcceptor` + `ForwardedOpen` + `drive_with_acceptor`)
|
||||||
and a minimal hand-rolled tracing subscriber (`trace_global` — the
|
and a minimal hand-rolled tracing subscriber (`trace_global`, the
|
||||||
family posture: no `tracing-subscriber` dependency; opted in per
|
family posture: no `tracing-subscriber` dependency; opted in per
|
||||||
row) as the leg-2 rows' failure-triage vehicle.
|
row) as the leg-2 rows' failure-triage vehicle.
|
||||||
- **Verification (all green)**: `cargo test` across default /
|
- **Verification (all green)**: `cargo test` across default /
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w4-channel-not-held-rows
|
id: w4-channel-not-held-rows
|
||||||
name: Wave 4 — channel-not-held guard wire rows (wave-3 gate booked item 2)
|
name: "Wave 4: channel-not-held guard wire rows (wave-3 gate booked item 2)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w4-git-exit-close]
|
depends_on: [w4-git-exit-close]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -14,25 +14,27 @@ level: implementation
|
|||||||
Close the wave-3 gate's booked finding item 2
|
Close the wave-3 gate's booked finding item 2
|
||||||
(`review-wave-3` §Booked findings): the **`channel-not-held` guard
|
(`review-wave-3` §Booked findings): the **`channel-not-held` guard
|
||||||
lacks wire test rows**. The guard is a documented decline class on
|
lacks wire test rows**. The guard is a documented decline class on
|
||||||
all three rows (git / tty pipe / shell) — a second exec (or request)
|
all three rows (git / tty pipe / shell): a second exec (or request)
|
||||||
on an already-served channel — constructible on the wire, but no row
|
on an already-served channel, constructible on the wire, but no row
|
||||||
asserts it in any build. `mapping.md`'s every-row-tested-in-every-build
|
asserts it in any build. `mapping.md`'s every-row-tested-in-every-build
|
||||||
convention names it contract territory; the fail-closed invariant is
|
convention names it contract territory; the fail-closed invariant is
|
||||||
contract on both sides of every feature (AGENTS.md convention 5).
|
contract on both sides of every feature (AGENTS.md convention 5).
|
||||||
|
|
||||||
**Test-only task** — no production code change expected (if landing
|
**Test-only task**, so no production code change is expected (if
|
||||||
|
landing
|
||||||
the rows exposes a guard gap, stop and book it for the gate rather
|
the rows exposes a guard gap, stop and book it for the gate rather
|
||||||
than silently widening scope):
|
than silently widening scope):
|
||||||
|
|
||||||
- Wire rows through the harness, **per build** (the per-build
|
- Wire rows through the harness, **per build** (the per-build
|
||||||
cfg-pinned module convention): a second exec on a served git
|
cfg-pinned module convention): a second exec on a served git
|
||||||
channel → the channel-not-held decline; the same on the tty pipe
|
channel → the channel-not-held decline; the same on the tty pipe
|
||||||
row and the shell row — the decline class asserted in every build
|
row and the shell row, with the decline class asserted in every
|
||||||
|
build
|
||||||
that carries the row (featured builds carry the serving rows; the
|
that carries the row (featured builds carry the serving rows; the
|
||||||
decline-class rows ride the same per-build matrix the dispatch
|
decline-class rows ride the same per-build matrix the dispatch
|
||||||
tests use).
|
tests use).
|
||||||
- The rows observe the decline's wire shape + the tracing event
|
- The rows observe the decline's wire shape + the tracing event
|
||||||
vocabulary (the `w3-decline-tracing` class/reason fields — the
|
vocabulary (the `w3-decline-tracing` class/reason fields, and the
|
||||||
rows double as the vocabulary's coverage for this class).
|
rows double as the vocabulary's coverage for this class).
|
||||||
- Depends on `w4-git-exit-close` only to keep `tests/git_mapping.rs`
|
- Depends on `w4-git-exit-close` only to keep `tests/git_mapping.rs`
|
||||||
single-writer (the hardening lands first; these rows land into the
|
single-writer (the hardening lands first; these rows land into the
|
||||||
@@ -43,7 +45,7 @@ than silently widening scope):
|
|||||||
- [x] Wire rows for the channel-not-held decline class on all three
|
- [x] Wire rows for the channel-not-held decline class on all three
|
||||||
rows (git / tty pipe / shell), per build, through the harness
|
rows (git / tty pipe / shell), per build, through the harness
|
||||||
- [x] The rows assert the wire shape + the tracing event class/reason
|
- [x] The rows assert the wire shape + the tracing event class/reason
|
||||||
- [x] No production code change (no guard gap exposed — nothing to
|
- [x] No production code change (no guard gap exposed, so nothing to
|
||||||
book; the guard's shape held on the wire in every featured
|
book; the guard's shape held on the wire in every featured
|
||||||
build)
|
build)
|
||||||
- [x] `cargo test` (+ per-feature + `--all-features`), clippy ×2
|
- [x] `cargo test` (+ per-feature + `--all-features`), clippy ×2
|
||||||
@@ -52,7 +54,7 @@ than silently widening scope):
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- tasks/implementation/review-wave-3.md §Booked findings item 2
|
- tasks/implementation/review-wave-3.md §Booked findings item 2
|
||||||
- docs/architecture/mapping.md (the request-level table — the
|
- docs/architecture/mapping.md (the request-level table, the
|
||||||
channel-not-held class; the every-row-tested convention)
|
channel-not-held class; the every-row-tested convention)
|
||||||
- tests/git_mapping.rs, tests/tty_mapping.rs, tests/dispatch_core.rs
|
- tests/git_mapping.rs, tests/tty_mapping.rs, tests/dispatch_core.rs
|
||||||
(the per-build matrix convention the rows join), src/observe.rs
|
(the per-build matrix convention the rows join), src/observe.rs
|
||||||
@@ -60,39 +62,40 @@ than silently widening scope):
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- **Test-only, as planned** — no production change; no guard gap
|
- **Test-only, as planned**, hence no production change; no guard
|
||||||
|
gap
|
||||||
surfaced (the landing verified the guard's documented shape on the
|
surfaced (the landing verified the guard's documented shape on the
|
||||||
wire in every build that carries each row).
|
wire in every build that carries each row).
|
||||||
- **Rows landed** (one per served row, each with the inline-decline
|
- **Rows landed** (one per served row, each with the inline-decline
|
||||||
wire shape — `channel_failure` while the first serving path rides
|
wire shape (`channel_failure` while the first serving path rides
|
||||||
on — plus the channel-stays-usable witness and the tracing event's
|
on), plus the channel-stays-usable witness and the tracing event's
|
||||||
exact request-decline vocabulary `src/observe.rs` carries):
|
exact request-decline vocabulary `src/observe.rs` carries):
|
||||||
- `tests/git_mapping.rs` (cfg `git`): a second exec on a served git
|
- `tests/git_mapping.rs` (cfg `git`): a second exec on a served git
|
||||||
channel — the decline event carries `exec_route = "git"`, and the
|
channel, whose decline event carries `exec_route = "git"`, and the
|
||||||
declined re-exec never spawns a second serving path (the doubles'
|
declined re-exec never spawns a second serving path (the doubles'
|
||||||
resolve count stays at one). Race-free by construction: the
|
resolve count stays at one). Race-free by construction: the
|
||||||
advertisement drains first and the serve parks at the doubles'
|
advertisement drains first and the serve parks at the doubles'
|
||||||
gated `list_refs` behind it, so the wire is quiescent for the
|
gated `list_refs` behind it, so the wire is quiescent for the
|
||||||
decline.
|
decline.
|
||||||
- `tests/tty_mapping.rs` (cfg `session`), two rows:
|
- `tests/tty_mapping.rs` (cfg `session`), two rows:
|
||||||
the pipe row (second exec on a served pipe channel — event
|
the pipe row (second exec on a served pipe channel, with the event
|
||||||
carries `exec_route = "pipe"`) and the shell row (second shell
|
carrying `exec_route = "pipe"`) and the shell row (second shell
|
||||||
request on a served shell channel — the event carries **no**
|
request on a served shell channel, whose event carries **no**
|
||||||
`exec_route` field, pinning the absent-field shape on the wire).
|
`exec_route` field, pinning the absent-field shape on the wire).
|
||||||
The exit-`None` shim script keeps the serve live and the wire
|
The exit-`None` shim script keeps the serve live and the wire
|
||||||
quiescent.
|
quiescent.
|
||||||
- **The per-build matrix**: the rows join the cfg-pinned convention —
|
- **The per-build matrix**: the rows join the cfg-pinned convention,
|
||||||
git-build rows run in every build carrying `git`, session rows in
|
git-build rows run in every build carrying `git`, session rows in
|
||||||
every build carrying `session` (verified across default / `git` /
|
every build carrying `session` (verified across default / `git` /
|
||||||
`session` / `session,git` / `tunnel` / `sftp` / `--all-features`).
|
`session` / `session,git` / `tunnel` / `sftp` / `--all-features`).
|
||||||
- **Shared capture helper**: `tests/common/event_capture.rs` (new) —
|
- **Shared capture helper**: `tests/common/event_capture.rs` (new) with
|
||||||
the observe-suite's recorded hand-rolled global-subscriber
|
the observe-suite's recorded hand-rolled global-subscriber
|
||||||
convention shared across the mapping binaries; `await_event`
|
convention shared across the mapping binaries; `await_event`
|
||||||
field-matching (parallel rows never order against each other) and
|
field-matching (parallel rows never order against each other) and
|
||||||
the exact-field-set assertion (`assert_request_decline`) — the
|
the exact-field-set assertion (`assert_request_decline`), and the
|
||||||
rows double as this class's vocabulary coverage, including the
|
rows double as this class's vocabulary coverage, including the
|
||||||
`debug` level and the build self-description.
|
`debug` level and the build self-description.
|
||||||
- **No `dispatch_core.rs` rows added** — that binary builds no seam,
|
- **No `dispatch_core.rs` rows added**, because that binary builds no seam,
|
||||||
so its git row declines at the config gate *before* the store-take;
|
so its git row declines at the config gate *before* the store-take;
|
||||||
the guard is unreachable there (the reference names the per-build
|
the guard is unreachable there (the reference names the per-build
|
||||||
convention the rows join, which the mapping binaries carry).
|
convention the rows join, which the mapping binaries carry).
|
||||||
@@ -102,12 +105,13 @@ than silently widening scope):
|
|||||||
The channel-not-held guard's wire rows landed on all three served
|
The channel-not-held guard's wire rows landed on all three served
|
||||||
rows: `second_exec_on_a_served_git_channel_declines_channel_not_held`
|
rows: `second_exec_on_a_served_git_channel_declines_channel_not_held`
|
||||||
(`tests/git_mapping.rs`), and the pipe/shell pair
|
(`tests/git_mapping.rs`), and the pipe/shell pair
|
||||||
(`tests/tty_mapping.rs`) — each pinning the inline decline shape
|
(`tests/tty_mapping.rs`), each pinning the inline decline shape
|
||||||
(`channel_failure`, no close, the first serve rides on, the channel
|
(`channel_failure`, no close, the first serve rides on, the channel
|
||||||
stays usable, no second serving path) plus the decline event's exact
|
stays usable, no second serving path) plus the decline event's exact
|
||||||
vocabulary (class `request`, reason `channel-not-held`, route
|
vocabulary (class `request`, reason `channel-not-held`, route
|
||||||
`git`/`pipe`/absent, `debug` level, self-describing build). No
|
`git`/`pipe`/absent, `debug` level, self-describing build). No
|
||||||
production change was needed — the guard held its documented shape in
|
production change was needed, and the guard held its documented
|
||||||
|
shape in
|
||||||
every featured build. `tests/common/event_capture.rs` shares the
|
every featured build. `tests/common/event_capture.rs` shares the
|
||||||
observe-suite's capture convention across the mapping binaries.
|
observe-suite's capture convention across the mapping binaries.
|
||||||
Verified: full suite + per-feature builds (default, `git`, `session`,
|
Verified: full suite + per-feature builds (default, `git`, `session`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w4-dial
|
id: w4-dial
|
||||||
name: Wave 4 — the client dial surface (russh client stack + the connection handle)
|
name: "Wave 4: the client dial surface (russh client stack + the connection handle)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [review-wave-3]
|
depends_on: [review-wave-3]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -11,49 +11,52 @@ level: implementation
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the client direction's substrate — the door outward
|
Land the client direction's substrate, the door facing outward
|
||||||
(`client.md` §The dial surface): the russh client stack every wave-4
|
(`client.md` §The dial surface): the russh client stack every wave-4
|
||||||
op row composes into, mirroring `w2-server-mount`'s role for the
|
op row composes into, mirroring `w2-server-mount`'s role for the
|
||||||
server direction. Everything later in the wave rides the authenticated
|
server direction. Everything later in the wave rides the authenticated
|
||||||
connection handle this task lands.
|
connection handle this task lands.
|
||||||
|
|
||||||
- **The dial surface** — `connect`-shaped entry taking the wave-1
|
- **The dial surface**: a `connect`-shaped entry taking the wave-1
|
||||||
[`DialConfig`](the address, the key source, the host-key policy, the
|
[`DialConfig`](the address, the key source, the host-key policy, the
|
||||||
`Limits`): the russh client `Config` built from the bounded knobs
|
`Limits`): the russh client `Config` built from the bounded knobs
|
||||||
(keepalive 15 s / 3 default posture per the reverse-runner evidence —
|
(keepalive 15 s / 3 default posture per the reverse-runner evidence
|
||||||
`client.md` §Correctness facts; inactivity; window/packet knobs per
|
and `client.md` §Correctness facts; inactivity; window/packet knobs per
|
||||||
`overview.md`'s table), the dial target as `DialConfig.address`
|
`overview.md`'s table), the dial target as `DialConfig.address`
|
||||||
carries it. If the dial surface needs a `DialConfig` amendment,
|
carries it. If the dial surface needs a `DialConfig` amendment,
|
||||||
land + record it here (the `w3-seam-config` course-correction
|
land + record it here (the `w3-seam-config` course-correction
|
||||||
pattern) — the wave-1 shape is expected to hold; decide and record.
|
pattern), while the wave-1 shape is expected to hold; decide and
|
||||||
- **Key source decrypt-at-dial** (ADR-005) — the [`KeySource`] load
|
record.
|
||||||
|
- **Key source decrypt-at-dial** (ADR-005), with the [`KeySource`]
|
||||||
|
load
|
||||||
happens at dial time; locked / absent / decryption-failure /
|
happens at dial time; locked / absent / decryption-failure /
|
||||||
unknown-`key_version` are all **no-dial outcomes** with their
|
unknown-`key_version` are all **no-dial outcomes** with their
|
||||||
distinct `KeySourceError` variants surfaced (never env vars, never
|
distinct `KeySourceError` variants surfaced (never env vars, never
|
||||||
plaintext at rest; the adapter-side `Zeroizing` wrap per OQ-07
|
plaintext at rest; the adapter-side `Zeroizing` wrap per OQ-07
|
||||||
already landed in wave 1 — consumed here).
|
already landed in wave 1, consumed here).
|
||||||
- **Host-key policy at `check_server_key`** (OQ-05) — the wave-1
|
- **Host-key policy at `check_server_key`** (OQ-05), the wave-1
|
||||||
[`HostKeyPolicy`] seam consumed at russh's client hook (the verdict
|
[`HostKeyPolicy`] seam consumed at russh's client hook (the verdict
|
||||||
type was pinned against this hook shape in `w1-host-key-policy`):
|
type was pinned against this hook shape in `w1-host-key-policy`):
|
||||||
the crate default ([`RejectAll`]) connects to nothing; case-1 pins
|
the crate default ([`RejectAll`]) connects to nothing; case-1 pins
|
||||||
hard-decline on mismatch; case-2 assembly policies decide. The
|
hard-decline on mismatch; case-2 assembly policies decide. The
|
||||||
crate is policy-blind — no accept-all built-in.
|
crate is policy-blind, with no accept-all built-in.
|
||||||
- **The client Handler skeleton at fail-closed defaults** — the
|
- **The client Handler skeleton at fail-closed defaults**: the
|
||||||
russh `client::Handler` impl the dial constructs: server-initiated
|
russh `client::Handler` impl the dial constructs: server-initiated
|
||||||
channel opens (`server_channel_open_forwarded_tcpip` /
|
channel opens (`server_channel_open_forwarded_tcpip` /
|
||||||
`_forwarded_streamlocal` / `_unknown` / x11 / agent) **declined
|
`_forwarded_streamlocal` / `_unknown` / x11 / agent) **declined
|
||||||
cleanly** — never russh's accept-and-drop default (ADR-003 §4, the
|
cleanly**, never russh's accept-and-drop default (ADR-003 §4, the
|
||||||
client-side symmetry rule; the acceptor registry that serves them
|
client-side symmetry rule; the acceptor registry that serves them
|
||||||
is `w4-reverse-listen`'s surface, this task pins the default).
|
is `w4-reverse-listen`'s surface, this task pins the default).
|
||||||
- **The authenticated connection handle** — the type everything else
|
- **The authenticated connection handle**, the type everything else
|
||||||
rides: the russh `client::Handle` wrapped with the crate's surface
|
rides: the russh `client::Handle` wrapped with the crate's surface
|
||||||
(publickey-only authentication from the key source — the family
|
(publickey-only authentication from the key source, the family
|
||||||
contract; no password/keyboard-interactive paths), the policy and
|
contract; no password/keyboard-interactive paths), the policy and
|
||||||
limits carried, the lifecycle primitives' raw material (full
|
limits carried, the lifecycle primitives' raw material (full
|
||||||
cancellation/clean-close shape is `w4-lifecycle`'s).
|
cancellation/clean-close shape is `w4-lifecycle`'s).
|
||||||
- **Test vehicle** — the client direction dials *vanilla* servers, so
|
- **Test vehicle**, because the client direction dials *vanilla*
|
||||||
|
servers, so
|
||||||
the loopback harness (alkssh server) is not the vehicle: land an
|
the loopback harness (alkssh server) is not the vehicle: land an
|
||||||
in-process russh **server double** (a fake `sshd` — hand-rolled
|
in-process russh **server double** (a fake `sshd`: a hand-rolled
|
||||||
`russh::server` handler in `tests/common`) as the contract-test
|
`russh::server` handler in `tests/common`) as the contract-test
|
||||||
vehicle, decide + record its placement and shape. Contract rows:
|
vehicle, decide + record its placement and shape. Contract rows:
|
||||||
dial success (authenticated), each key-source failure posture
|
dial success (authenticated), each key-source failure posture
|
||||||
@@ -62,7 +65,7 @@ connection handle this task lands.
|
|||||||
wiring observable, server-initiated open declined cleanly.
|
wiring observable, server-initiated open declined cleanly.
|
||||||
- **The smoke posture** decided + recorded (an alkssh-client ↔
|
- **The smoke posture** decided + recorded (an alkssh-client ↔
|
||||||
alkssh-server in-process loopback is the cheap real-stack smoke;
|
alkssh-server in-process loopback is the cheap real-stack smoke;
|
||||||
a real-`sshd` dial is wave-5 e2e territory — never silently
|
a real-`sshd` dial is wave-5 e2e territory, never silently
|
||||||
dropped).
|
dropped).
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
@@ -92,10 +95,11 @@ connection handle this task lands.
|
|||||||
surface), docs/architecture/decisions/003-feature-graph.md (§4 the
|
surface), docs/architecture/decisions/003-feature-graph.md (§4 the
|
||||||
client-side fail-closed acceptor default this skeleton pins),
|
client-side fail-closed acceptor default this skeleton pins),
|
||||||
docs/architecture/decisions/005-key-sources.md (decrypt-at-dial)
|
docs/architecture/decisions/005-key-sources.md (decrypt-at-dial)
|
||||||
- docs/research/russh.md §Client surface (`client::Handler` inventory
|
- docs/research/russh.md §Client surface (`client::Handler` inventory,
|
||||||
— `check_server_key` defaults to refuse; the
|
where `check_server_key` defaults to refuse; the
|
||||||
`server_channel_open_*` hook set; `client::Config` knobs),
|
`server_channel_open_*` hook set; `client::Config` knobs),
|
||||||
docs/research/reverse-runner.md (lessons 3/5 — keepalives
|
docs/research/reverse-runner.md (lessons 3/5, with the
|
||||||
|
keepalives
|
||||||
load-bearing, the ephemeral-fleet policy posture)
|
load-bearing, the ephemeral-fleet policy posture)
|
||||||
- docs/research/poc-1-auth-bridge.md §6/§7 (the client-side plumbing
|
- docs/research/poc-1-auth-bridge.md §6/§7 (the client-side plumbing
|
||||||
facts), docs/research/poc-2-vault-keys.md (the dial lifecycle)
|
facts), docs/research/poc-2-vault-keys.md (the dial lifecycle)
|
||||||
@@ -106,19 +110,19 @@ connection handle this task lands.
|
|||||||
|
|
||||||
### What landed (2026-10-29)
|
### What landed (2026-10-29)
|
||||||
|
|
||||||
- `src/client.rs`: the dial surface — `dial(DialConfig)` over
|
- `src/client.rs`: the dial surface, `dial(DialConfig)` over
|
||||||
[`DialConfig`], the four-step sequence (key load → bounded russh
|
[`DialConfig`], the four-step sequence (key load → bounded russh
|
||||||
`Config` → policy at `check_server_key` → single publickey
|
`Config` → policy at `check_server_key` → single publickey
|
||||||
auth), [`SshConnection`] (the connection handle: `limits()`
|
auth), [`SshConnection`] (the connection handle: `limits()`
|
||||||
/ `address()` / `username()` / `is_closed()` / `close()` — the
|
/ `address()` / `username()` / `is_closed()` / `close()`, the
|
||||||
OQ-12 raw material; the full lifecycle surface is `w4-lifecycle`'s),
|
OQ-12 raw material; the full lifecycle surface is `w4-lifecycle`'s),
|
||||||
the address parse (`host:port` + the bracket form;
|
the address parse (`host:port` + the bracket form;
|
||||||
`DialError::Address` on refusal — dial-time, the family's
|
`DialError::Address` on refusal (dial-time, matching the family's
|
||||||
no-construction-validation shape), and the doc-hidden
|
no-construction-validation shape), and the doc-hidden
|
||||||
`russh_client_config` mapping (the bounded-config observable).
|
`russh_client_config` mapping (the bounded-config observable).
|
||||||
- `src/client_handler.rs`: the client Handler skeleton (the
|
- `src/client_handler.rs`: the client Handler skeleton (the
|
||||||
mirror of `handler.rs`'s server-direction handler; a separate
|
mirror of `handler.rs`'s server-direction handler; a separate
|
||||||
module because `w4-op-rows` / `w4-reverse-listen` grow it) —
|
module because `w4-op-rows` / `w4-reverse-listen` grow it), with
|
||||||
`check_server_key` wired to the injected policy
|
`check_server_key` wired to the injected policy
|
||||||
(certificates fingerprint their inner key; the verdict passes
|
(certificates fingerprint their inner key; the verdict passes
|
||||||
through), every `server_channel_open_*` hook overridden
|
through), every `server_channel_open_*` hook overridden
|
||||||
@@ -126,16 +130,16 @@ connection handle this task lands.
|
|||||||
`ChannelOpenFailure::AdministrativelyProhibited` reject (never
|
`ChannelOpenFailure::AdministrativelyProhibited` reject (never
|
||||||
russh's accept-and-drop default), `should_accept_unknown_server_channel`
|
russh's accept-and-drop default), `should_accept_unknown_server_channel`
|
||||||
the explicit `false`, decline events on the crate vocabulary.
|
the explicit `false`, decline events on the crate vocabulary.
|
||||||
- `src/error.rs`: [`DialError`] — the dial direction's closed
|
- `src/error.rs`: [`DialError`], the dial direction's closed
|
||||||
taxonomy (`KeySource` per-variant preserved / `Address` /
|
taxonomy (`KeySource` per-variant preserved / `Address` /
|
||||||
`HostKeyRejected` / `AuthRejected` / `Wire`), with a manual
|
`HostKeyRejected` / `AuthRejected` / `Wire`), with a manual
|
||||||
`From<russh::Error>` that upgrades russh's `UnknownKey` — the only
|
`From<russh::Error>` that upgrades russh's `UnknownKey` (the only
|
||||||
producer of which is the `check_server_key` decline path — into
|
producer of which is the `check_server_key` decline path) into
|
||||||
`HostKeyRejected`; hence the `Wire` variant carries no `#[from]`
|
`HostKeyRejected`; hence the `Wire` variant carries no `#[from]`
|
||||||
(one conversion, one place).
|
(one conversion, one place).
|
||||||
- `src/config.rs`: the **DialConfig amendment** (below) + doc
|
- `src/config.rs`: the **DialConfig amendment** (below) + doc
|
||||||
updates; `src/observe.rs`: the client-side decline reasons
|
updates; `src/observe.rs`: the client-side decline reasons
|
||||||
(`no-acceptor` — the skeleton's decline-all default; the registry
|
(`no-acceptor`, the skeleton's decline-all default; the registry
|
||||||
is `w4-reverse-listen`'s) recorded in the vocabulary; `src/lib.rs`:
|
is `w4-reverse-listen`'s) recorded in the vocabulary; `src/lib.rs`:
|
||||||
the re-exports (`dial` / `SshConnection` / `DialError` public; the
|
the re-exports (`dial` / `SshConnection` / `DialError` public; the
|
||||||
handler + config mapping `#[doc(hidden)]` test surface, the
|
handler + config mapping `#[doc(hidden)]` test surface, the
|
||||||
@@ -145,33 +149,33 @@ connection handle this task lands.
|
|||||||
|
|
||||||
### Decided + recorded
|
### Decided + recorded
|
||||||
|
|
||||||
1. **The DialConfig amendment (`username`) — the task's sanctioned
|
1. **The DialConfig amendment (`username`), the task's sanctioned
|
||||||
course-correction, the only one.** The wave-1 shape carried
|
course-correction, the only one.** The wave-1 shape carried
|
||||||
everything `client.md` §The dial surface names, but the
|
everything `client.md` §The dial surface names, but the
|
||||||
USERAUTH_REQUEST structurally requires a user string
|
USERAUTH_REQUEST structurally requires a user string
|
||||||
(`authenticate_publickey(user, key)`) and the remote end evaluates
|
(`authenticate_publickey(user, key)`) and the remote end evaluates
|
||||||
it — no dial can exist without one. Decided with the
|
it, and no dial can exist without one. Decided with the
|
||||||
`w3-seam-config` pattern: `username: String` is a required
|
`w3-seam-config` pattern: `username: String` is a required
|
||||||
`DialConfig::new` parameter (no crate default — a default account
|
`DialConfig::new` parameter (no crate default, since a default account
|
||||||
would be deployment policy smuggled into the crate; the empty
|
would be deployment policy smuggled into the crate; the empty
|
||||||
string is accepted as data and is the remote end's to refuse). It
|
string is accepted as data and is the remote end's to refuse). It
|
||||||
is *not identity* — the identity model stays fingerprint-only
|
is *not identity*, as the identity model stays fingerprint-only
|
||||||
(ADR-004 decision 5, mirrored client-side), it is never logged
|
(ADR-004 decision 5, mirrored client-side), it is never logged
|
||||||
(the event vocabulary carries no username), and it rides
|
(the event vocabulary carries no username), and it rides
|
||||||
[`SshConnection`] as carried dial data. Wave-1 records amended
|
[`SshConnection`] as carried dial data. Wave-1 records amended
|
||||||
(the config module + field docs).
|
(the config module + field docs).
|
||||||
2. **The server double's placement/shape**
|
2. **The server double's placement/shape**
|
||||||
(`tests/common/sshd_double.rs`): a plain russh server (no alkssh
|
(`tests/common/sshd_double.rs`): a plain russh server (no alkssh
|
||||||
in its serving path — vanilla-parity) on a **real loopback TCP
|
in its serving path, hence vanilla-parity) on a **real loopback TCP
|
||||||
listener** (`127.0.0.1:0` — a dial target is an address the surface
|
listener** at `127.0.0.1:0` (a dial target is an address the surface
|
||||||
resolves and connects to; the duplex-pair trick is the
|
resolves and connects to; the duplex-pair trick is the
|
||||||
server-direction harness's, and a dial has no `&mut Handle` slot
|
server-direction harness's, and a dial has no `&mut Handle` slot
|
||||||
to attach one to). One dial per instance (doubles are cheap); the
|
to attach one to). One dial per instance (doubles are cheap); the
|
||||||
handler resolves fingerprints against an injected accept set (the
|
handler resolves fingerprints against an injected accept set (the
|
||||||
vanilla-`authorized_keys` stand-in) with every other auth hook an
|
vanilla-`authorized_keys` stand-in) with every other auth hook an
|
||||||
explicit reject; the `auth_succeeded` hook hands the row the
|
explicit reject; the `auth_succeeded` hook hands the row the
|
||||||
per-connection `russh::server::Handle` through a one-shot carrier —
|
per-connection `russh::server::Handle` through a one-shot carrier,
|
||||||
the initiator for the server-initiated-open decline rows (the
|
which is the initiator for the server-initiated-open decline rows (the
|
||||||
double calls `channel_open_*` at the client and observes each as
|
double calls `channel_open_*` at the client and observes each as
|
||||||
`Err(ChannelOpenFailure(AdministrativelyProhibited))`); a
|
`Err(ChannelOpenFailure(AdministrativelyProhibited))`); a
|
||||||
per-session outcome channel is the clean-close rows' observable;
|
per-session outcome channel is the clean-close rows' observable;
|
||||||
@@ -180,14 +184,14 @@ connection handle this task lands.
|
|||||||
3. **The smoke posture**: an alkssh-client ↔ alkssh-server
|
3. **The smoke posture**: an alkssh-client ↔ alkssh-server
|
||||||
in-process loopback over a real TCP socket, landed as the row
|
in-process loopback over a real TCP socket, landed as the row
|
||||||
`smoke_alkssh_client_dials_the_alkssh_server_direction`
|
`smoke_alkssh_client_dials_the_alkssh_server_direction`
|
||||||
(`tests/client_dial.rs` — both russh stacks, the full auth bridge
|
(`tests/client_dial.rs`, with both russh stacks, the full auth bridge
|
||||||
server-side, the full custody + policy chain client-side). A
|
server-side, the full custody + policy chain client-side). A
|
||||||
real-`sshd` dial is wave-5 e2e territory (`docs/plans/
|
real-`sshd` dial is wave-5 e2e territory (`docs/plans/
|
||||||
implementation.md` §Wave 5); nothing is silently dropped.
|
implementation.md` §Wave 5); nothing is silently dropped.
|
||||||
4. **The decline-class mapping**: russh surfaces a policy decline as
|
4. **The decline-class mapping**: russh surfaces a policy decline as
|
||||||
its `UnknownKey` wire error; since the client's `check_server_key`
|
its `UnknownKey` wire error; since the client's `check_server_key`
|
||||||
hook is the only producer of that class, the manual `From` makes
|
hook is the only producer of that class, the manual `From` makes
|
||||||
it the semantic [`DialError::HostKeyRejected`] — every other russh
|
it the semantic [`DialError::HostKeyRejected`], while every other russh
|
||||||
error stays [`DialError::Wire`].
|
error stays [`DialError::Wire`].
|
||||||
5. **The skeleton's unknown-type gate** (`should_accept_unknown_
|
5. **The skeleton's unknown-type gate** (`should_accept_unknown_
|
||||||
server_channel` → explicit `false`) is code-pinned, not
|
server_channel` → explicit `false`) is code-pinned, not
|
||||||
@@ -199,25 +203,25 @@ connection handle this task lands.
|
|||||||
wire-row-pinned; the `_unknown` open hook is a defensive reject
|
wire-row-pinned; the `_unknown` open hook is a defensive reject
|
||||||
behind the gate.
|
behind the gate.
|
||||||
6. **Auth posture**: exactly one `authenticate_publickey` attempt
|
6. **Auth posture**: exactly one `authenticate_publickey` attempt
|
||||||
(ed25519-only, ADR-002 decision 4 — `None` is the whole
|
(ed25519-only, ADR-002 decision 4, `None` being the whole
|
||||||
hash-algorithm posture); a refusal closes the attempt cleanly
|
hash-algorithm posture); a refusal closes the attempt cleanly
|
||||||
(`Disconnect::ByApplication`) and surfaces
|
(`Disconnect::ByApplication`) and surfaces
|
||||||
[`DialError::AuthRejected`]. russh's other auth methods
|
[`DialError::AuthRejected`]. russh's other auth methods
|
||||||
(password / kbi / cert / gss / signer-injection — the signer seam
|
(password / kbi / cert / gss / signer-injection, with the signer
|
||||||
is dismissed by ADR-005 decision 8) are never reached from
|
seam dismissed by ADR-005 decision 8) are never reached from
|
||||||
[`SshConnection`].
|
[`SshConnection`].
|
||||||
7. **Bounded posture**: the knob mapping keeps the crate's documented
|
7. **Bounded posture**: the knob mapping keeps the crate's documented
|
||||||
postures (`keepalive` 15 s / 3, the wall clock always `Some`,
|
postures (`keepalive` 15 s / 3, the wall clock always `Some`,
|
||||||
window/packet/buffer verbatim) and leaves
|
window/packet/buffer verbatim) and leaves
|
||||||
russh's own default postures untouched (rekey `Limits`,
|
russh's own default postures untouched (rekey `Limits`,
|
||||||
preferred algorithms, `client_id`, `nodelay` — no client-side
|
preferred algorithms, `client_id`, `nodelay`. No client-side
|
||||||
auth-attempt knob exists at this pin and the single-attempt
|
auth-attempt knob exists at this pin and the single-attempt
|
||||||
contract makes none needed).
|
contract makes none needed).
|
||||||
|
|
||||||
### Verification
|
### Verification
|
||||||
|
|
||||||
`cargo test` (default) and `cargo test --all-features` (and
|
`cargo test` (default) and `cargo test --all-features` (and
|
||||||
per-feature `--features {session,git,tunnel}`) all green — 14 test
|
per-feature `--features {session,git,tunnel}`) all green, with 14 test
|
||||||
binaries each, the new `tests/client_dial.rs` carrying 10 rows;
|
binaries each, the new `tests/client_dial.rs` carrying 10 rows;
|
||||||
`cargo clippy --all-features --all-targets` and `cargo clippy
|
`cargo clippy --all-features --all-targets` and `cargo clippy
|
||||||
--all-targets` both `-D warnings` clean; `cargo fmt --check` clean;
|
--all-targets` both `-D warnings` clean; `cargo fmt --check` clean;
|
||||||
@@ -229,11 +233,11 @@ The client direction's substrate landed: `dial(config) ->
|
|||||||
Result<SshConnection, DialError>` runs the fail-closed four-step
|
Result<SshConnection, DialError>` runs the fail-closed four-step
|
||||||
sequence (decrypt-at-dial key load with every no-dial posture
|
sequence (decrypt-at-dial key load with every no-dial posture
|
||||||
distinct; the bounded russh client `Config` from `Limits`; the
|
distinct; the bounded russh client `Config` from `Limits`; the
|
||||||
injected host-key policy at `check_server_key` — `RejectAll`
|
injected host-key policy at `check_server_key`, where `RejectAll`
|
||||||
connects to nothing; a single publickey-only auth attempt) and
|
connects to nothing; a single publickey-only auth attempt) and
|
||||||
returns the authenticated connection handle everything later in the
|
returns the authenticated connection handle everything later in the
|
||||||
wave rides. One sanctioned `DialConfig` amendment: the required
|
wave rides. One sanctioned `DialConfig` amendment: the required
|
||||||
`username` field (the USERAUTH_REQUEST's user string — no crate
|
`username` field (the USERAUTH_REQUEST's user string; no crate
|
||||||
default, never identity, never logged). The fail-closed client
|
default, never identity, never logged). The fail-closed client
|
||||||
Handler skeleton declines every server-initiated open on the wire
|
Handler skeleton declines every server-initiated open on the wire
|
||||||
(the fake-`sshd` double in `tests/common` pins six hook rows +
|
(the fake-`sshd` double in `tests/common` pins six hook rows +
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w4-git-exit-close
|
id: w4-git-exit-close
|
||||||
name: Wave 4 — git row hardening (status-before-close by construction — wave-3 gate booked item 1)
|
name: "Wave 4: git row hardening (status-before-close by construction, wave-3 gate booked item 1)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w3-git-mapping]
|
depends_on: [w3-git-mapping]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -15,7 +15,7 @@ Close the wave-3 gate's only structural booked finding
|
|||||||
(`review-wave-3` §Booked findings item 1): the git row's
|
(`review-wave-3` §Booked findings item 1): the git row's
|
||||||
status-before-close is honored **by scheduling, not by construction**.
|
status-before-close is honored **by scheduling, not by construction**.
|
||||||
Today `serve_duplex_resolved` splits the channel stream internally and
|
Today `serve_duplex_resolved` splits the channel stream internally and
|
||||||
its session halves drop when the serve returns — russh's
|
its session halves drop when the serve returns, so russh's
|
||||||
`ChannelCloseOnDrop` rx-half emits a server→client `CHANNEL_CLOSE`
|
`ChannelCloseOnDrop` rx-half emits a server→client `CHANNEL_CLOSE`
|
||||||
*inside* the seam call, before `serve_git_row`'s explicit
|
*inside* the seam call, before `serve_git_row`'s explicit
|
||||||
`exit_status_request`. The ordering currently holds (the task's
|
`exit_status_request`. The ordering currently holds (the task's
|
||||||
@@ -23,26 +23,27 @@ continuation queues the report in the same poll, ahead of the drop's
|
|||||||
send) and every wire row passes, but a full `Sender<Msg>` buffer at
|
send) and every wire row passes, but a full `Sender<Msg>` buffer at
|
||||||
the serve tail could order the implicit close first and silently drop
|
the serve tail could order the implicit close first and silently drop
|
||||||
the report (russh no-ops `exit_status_request` for an already-closed
|
the report (russh no-ops `exit_status_request` for an already-closed
|
||||||
channel) — a silent exit-report loss on a published row.
|
channel), a silent exit-report loss on a published row.
|
||||||
|
|
||||||
The fix wants a seam/shape change — **decide + record which**:
|
The fix wants a seam/shape change (decide and record which):
|
||||||
|
|
||||||
- split the channel halves in `serve_git_row` and pass them into the
|
- split the channel halves in `serve_git_row` and pass them into the
|
||||||
seam (the report then rides a half the seam never closes), or
|
seam (the report then rides a half the seam never closes), or
|
||||||
- queue the exit report while the stream is alive (the report's send
|
- queue the exit report while the stream is alive (the report's send
|
||||||
ordered before any close-contracting drop by construction), or
|
ordered before any close-contracting drop by construction), or
|
||||||
- another shape that makes the ordering structural rather than
|
- another shape that makes the ordering structural rather than
|
||||||
scheduler-luck — the gate's reasoning and the two named options are
|
scheduler-luck; the gate's reasoning and the two named options are
|
||||||
the starting points, not the bounds.
|
the starting points, not the bounds.
|
||||||
|
|
||||||
Constraints: every existing git wire row stays green (no behavior
|
Constraints: every existing git wire row stays green (no behavior
|
||||||
change on the happy path — this is ordering hardening, not semantics
|
change on the happy path, since this is ordering hardening, not
|
||||||
change); the spawn-and-return constraint (ADR-006 decision 4) holds —
|
semantics
|
||||||
the reporting still rides the spawned task; the tty rows are NOT in
|
change); the spawn-and-return constraint (ADR-006 decision 4) holds,
|
||||||
|
because the reporting still rides the spawned task; the tty rows are NOT in
|
||||||
this class (their report precedes any close-contracting drop in one
|
this class (their report precedes any close-contracting drop in one
|
||||||
task — the gate verified; don't touch them). New wire row(s) pinning
|
task, which the gate verified; don't touch them). New wire row(s) pinning
|
||||||
the construction: a saturated `Sender<Msg>` buffer at the serve tail
|
the construction: a saturated `Sender<Msg>` buffer at the serve tail
|
||||||
(the failure scenario made real) still delivers the exit report —
|
(the failure scenario made real) still delivers the exit report,
|
||||||
the by-construction property proven on the wire, not by reasoning.
|
the by-construction property proven on the wire, not by reasoning.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
@@ -61,10 +62,10 @@ the by-construction property proven on the wire, not by reasoning.
|
|||||||
|
|
||||||
- tasks/implementation/review-wave-3.md §Booked findings item 1 (the
|
- tasks/implementation/review-wave-3.md §Booked findings item 1 (the
|
||||||
full finding + the two named fix shapes)
|
full finding + the two named fix shapes)
|
||||||
- docs/architecture/mapping.md (the git exec row — exit-status both
|
- docs/architecture/mapping.md (the git exec row, exit-status both
|
||||||
directions), docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
directions), docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
||||||
(decision 4 — spawn-and-return)
|
(decision 4, spawn-and-return)
|
||||||
- src/git_map.rs, src/serve.rs (`report_exit_status_then_close` — the
|
- src/git_map.rs, src/serve.rs (`report_exit_status_then_close`, the
|
||||||
wave-2 reporting mechanics this hardening composes with), alkgit
|
wave-2 reporting mechanics this hardening composes with), alkgit
|
||||||
0.1.0 `serve_duplex_resolved` (the seam whose half-ownership is the
|
0.1.0 `serve_duplex_resolved` (the seam whose half-ownership is the
|
||||||
question)
|
question)
|
||||||
@@ -73,8 +74,8 @@ the by-construction property proven on the wire, not by reasoning.
|
|||||||
|
|
||||||
### The shape decided (2026-10-29)
|
### The shape decided (2026-10-29)
|
||||||
|
|
||||||
Went with the first named option — **split the channel halves in
|
Went with the first named option, **splitting the channel halves in
|
||||||
`serve_git_row` and pass them into the seam** — refined by one russh
|
`serve_git_row` and passing them into the seam**, refined by one russh
|
||||||
0.65 reality: `russh::channels::io::{ChannelRx, ChannelTx}` are
|
0.65 reality: `russh::channels::io::{ChannelRx, ChannelTx}` are
|
||||||
*private* at this pin, so the split-halves duplex is rebuilt from the
|
*private* at this pin, so the split-halves duplex is rebuilt from the
|
||||||
public API in `src/git_map.rs`:
|
public API in `src/git_map.rs`:
|
||||||
@@ -83,14 +84,14 @@ public API in `src/git_map.rs`:
|
|||||||
[`ChannelHalveReader`]: the owned `ChannelReadHalf`
|
[`ChannelHalveReader`]: the owned `ChannelReadHalf`
|
||||||
(`channel.split()`), a `poll_read` state machine whose observation
|
(`channel.split()`), a `poll_read` state machine whose observation
|
||||||
semantics repin russh's private `ChannelRx` *without* the
|
semantics repin russh's private `ChannelRx` *without* the
|
||||||
`ChannelCloseOnDrop` wrapper — `Data` fills (partial reads
|
`ChannelCloseOnDrop` wrapper, so `Data` fills (partial reads
|
||||||
buffered), `ExtendedData`/other non-data messages swallowed and
|
buffered), `ExtendedData`/other non-data messages swallowed and
|
||||||
re-read (the `_`-arm parity), `Eof` ends the stream, the peer's
|
re-read (the `_`-arm parity), `Eof` ends the stream, the peer's
|
||||||
`Close` leaves the reader to the channel's sender-death EOF
|
`Close` leaves the reader to the channel's sender-death EOF
|
||||||
(`wait()` → `None`). The `wait()` access is *future-owned*: each
|
(`wait()` → `None`). The `wait()` access is *future-owned*: each
|
||||||
read round-trips the half through a boxed future that parks it
|
read round-trips the half through a boxed future that parks it
|
||||||
inside `ChannelReadHalf::wait` and hands it back with the observed
|
inside `ChannelReadHalf::wait` and hands it back with the observed
|
||||||
message — fully owned (`'static`-usable, the seam's `T` bound)
|
message, fully owned (`'static`-usable, the seam's `T` bound)
|
||||||
across every park/buffer/drop state, no extra task, and **no
|
across every park/buffer/drop state, no extra task, and **no
|
||||||
`Drop` path ever messages the wire**.
|
`Drop` path ever messages the wire**.
|
||||||
- **The writer** is the write half's `ChannelTx` via
|
- **The writer** is the write half's `ChannelTx` via
|
||||||
@@ -101,20 +102,20 @@ public API in `src/git_map.rs`:
|
|||||||
the seam's `'static` bound satisfiable without parking the half.
|
the seam's `'static` bound satisfiable without parking the half.
|
||||||
`poll_shutdown` still sends EOF (exactly `into_stream`'s).
|
`poll_shutdown` still sends EOF (exactly `into_stream`'s).
|
||||||
|
|
||||||
The result: **nothing the seam drops ever messages the wire** — the
|
The result: **nothing the seam drops ever messages the wire**, with the
|
||||||
report's `exit_status_request` send is the sole close-contracting
|
report's `exit_status_request` send is the sole close-contracting
|
||||||
sender, and its queue position ahead of the explicit close holds by
|
sender, and its queue position ahead of the explicit close holds by
|
||||||
construction (sequential awaited sends on the one `Handle`),
|
construction (sequential awaited sends on the one `Handle`),
|
||||||
saturated buffer or not. The hazard's premise (russh's
|
saturated buffer or not. The hazard's premise (russh's
|
||||||
`ChannelCloseOnDrop` `Drop` spawning a `CHANNEL_CLOSE` send that can
|
`ChannelCloseOnDrop` `Drop` spawning a `CHANNEL_CLOSE` send that can
|
||||||
win a queue position on the bounded `Sender<Msg>` —
|
win a queue position on the bounded `Sender<Msg>`
|
||||||
`event_buffer_size`, default 10 — before the report's) is *removed*,
|
(`event_buffer_size`, default 10) before the report's, is *removed*,
|
||||||
not raced.
|
not raced.
|
||||||
|
|
||||||
### The wire row (the by-construction property pinned)
|
### The wire row (the by-construction property pinned)
|
||||||
|
|
||||||
`tests/git_mapping.rs::backlogged_seam_tail_still_reports_exit_status_before_close`
|
`tests/git_mapping.rs::backlogged_seam_tail_still_reports_exit_status_before_close`,
|
||||||
— the finding's failure scenario made real on the wire: the serve's
|
the finding's failure scenario made real on the wire: the serve's
|
||||||
answer volume (300 one-pkt-line ref answers) exceeds the entire
|
answer volume (300 one-pkt-line ref answers) exceeds the entire
|
||||||
outbound pipeline via the new client-direction data gate
|
outbound pipeline via the new client-direction data gate
|
||||||
(`tests/common/harness.rs::DataGate` + the harness handler's `data`
|
(`tests/common/harness.rs::DataGate` + the harness handler's `data`
|
||||||
@@ -125,18 +126,18 @@ serve's remaining answer writes pend (the serve tail blocked). The
|
|||||||
gate's release then drives the serve tail through the just-unwinding
|
gate's release then drives the serve tail through the just-unwinding
|
||||||
contention, and the row pins: the whole backlogged answer drains
|
contention, and the row pins: the whole backlogged answer drains
|
||||||
**through the same queue the report rides**, then exit-status(0), then
|
**through the same queue the report rides**, then exit-status(0), then
|
||||||
close — order asserted, exit code asserted.
|
close; the order is asserted, the exit code asserted.
|
||||||
|
|
||||||
**The row falsifies the old shape.** With a temporary one-line revert
|
**The row falsifies the old shape.** With a temporary one-line revert
|
||||||
of `serve_git_row` to `channel.into_stream()` the row FAILED
|
of `serve_git_row` to `channel.into_stream()` the row FAILED
|
||||||
**15/15** runs — the implicit close won the frozen-drain queue race
|
**15/15** runs, as the implicit close won the frozen-drain queue race
|
||||||
("expected exit-status(0), got Some(Close)"), the silent report loss
|
("expected exit-status(0), got Some(Close)"), the silent report loss
|
||||||
the gate booked. With the split-halves shape: **10/10 green**. So the
|
the gate booked. With the split-halves shape: **10/10 green**. So the
|
||||||
row is a two-sided pin: deterministic red under the hazard, green by
|
row is a two-sided pin: deterministic red under the hazard, green by
|
||||||
construction without it.
|
construction without it.
|
||||||
|
|
||||||
Test-vehicle additions (test surface, freely versioned): the doubles'
|
Test-vehicle additions (test surface, freely versioned): the doubles'
|
||||||
per-repo serve gate (`tests/common/git_doubles.rs`: `install_gate` —
|
per-repo serve gate (`tests/common/git_doubles.rs`: `install_gate`, a
|
||||||
race-safe double-check `MemGate`, gating `GitRefs::list_refs`, so the
|
race-safe double-check `MemGate`, gating `GitRefs::list_refs`, so the
|
||||||
row schedules the serve's stages against prepared wire state) and the
|
row schedules the serve's stages against prepared wire state) and the
|
||||||
harness's `DataGate` + `drive_with_data_gate` (the `drive*` bodies
|
harness's `DataGate` + `drive_with_data_gate` (the `drive*` bodies
|
||||||
@@ -145,15 +146,15 @@ signatures unchanged).
|
|||||||
|
|
||||||
### Constraints check
|
### Constraints check
|
||||||
|
|
||||||
- **Every existing git wire row green** — the full git suite
|
- **Every existing git wire row green**, with the full git suite
|
||||||
(`tests/git_mapping.rs`, 12 rows) and every other loopback suite:
|
(`tests/git_mapping.rs`, 12 rows) and every other loopback suite:
|
||||||
no behavior change on the happy path (the wire order the rows pin —
|
no behavior change on the happy path (the wire order the rows pin,
|
||||||
data, exit-status, close — is identical under both shapes when the
|
i.e. data, exit-status, close, is identical under both shapes when
|
||||||
race doesn't fire).
|
the race doesn't fire).
|
||||||
- **Spawn-and-return intact** (ADR-006 decision 4): the reporting
|
- **Spawn-and-return intact** (ADR-006 decision 4): the reporting
|
||||||
still rides the spawned serving task through the cloned
|
still rides the spawned serving task through the cloned
|
||||||
`Session::handle()`; `serve.rs`'s mechanics untouched.
|
`Session::handle()`; `serve.rs`'s mechanics untouched.
|
||||||
- **The tty rows untouched** (not in this class — the gate verified).
|
- **The tty rows untouched** (not in this class; the gate verified that).
|
||||||
- **No API/limits change**: `event_buffer_size` stays russh's default
|
- **No API/limits change**: `event_buffer_size` stays russh's default
|
||||||
posture (the adapter docs' recorded default posture preserved); the
|
posture (the adapter docs' recorded default posture preserved); the
|
||||||
row pins the default's value with a note that a russh default
|
row pins the default's value with a note that a russh default
|
||||||
@@ -163,7 +164,7 @@ signatures unchanged).
|
|||||||
|
|
||||||
**Completed.** The git row's status-before-close now holds **by
|
**Completed.** The git row's status-before-close now holds **by
|
||||||
construction**: `serve_git_row` no longer hands russh's
|
construction**: `serve_git_row` no longer hands russh's
|
||||||
close-on-drop `ChannelStream` (`into_stream()`) to the seam — the
|
close-on-drop `ChannelStream` (`into_stream()`) to the seam; the
|
||||||
seam rides a split-halves duplex (`git_channel_duplex` →
|
seam rides a split-halves duplex (`git_channel_duplex` →
|
||||||
`ChannelHalveReader` over the owned read half + the write half's
|
`ChannelHalveReader` over the owned read half + the write half's
|
||||||
`ChannelTx` via the capture-erased trait object) where **no drop
|
`ChannelTx` via the capture-erased trait object) where **no drop
|
||||||
@@ -173,8 +174,8 @@ unconditional (russh 0.65's private `ChannelRx`/`ChannelTx` noted;
|
|||||||
the semantics repinned from the public API). New wire row pins the
|
the semantics repinned from the public API). New wire row pins the
|
||||||
by-construction property on a genuinely saturated tail (volume flood
|
by-construction property on a genuinely saturated tail (volume flood
|
||||||
→ frozen client loop → duplex → bounded message queue; gate release
|
→ frozen client loop → duplex → bounded message queue; gate release
|
||||||
→ the tail through the unwinding contention) — and falsifies the old
|
→ the tail through the unwinding contention), and it also falsifies
|
||||||
shape deterministically (15/15 red under a temporary
|
the old shape deterministically (15/15 red under a temporary
|
||||||
`into_stream()` revert, 10/10 green with the fix). Doubles gained a
|
`into_stream()` revert, 10/10 green with the fix). Doubles gained a
|
||||||
race-safe per-repo serve gate; the harness gained the client-side
|
race-safe per-repo serve gate; the harness gained the client-side
|
||||||
data gate + `drive_with_data_gate` (the `drive*` cores factored,
|
data gate + `drive_with_data_gate` (the `drive*` cores factored,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w4-lifecycle
|
id: w4-lifecycle
|
||||||
name: Wave 4 — lifecycle primitives (cancellable handles + clean disconnect)
|
name: "Wave 4: lifecycle primitives (cancellable handles + clean disconnect)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w4-op-rows, w4-reverse-listen]
|
depends_on: [w4-op-rows, w4-reverse-listen]
|
||||||
scope: narrow
|
scope: narrow
|
||||||
@@ -13,24 +13,24 @@ level: implementation
|
|||||||
|
|
||||||
Land the OQ-12 resolution in code (`client.md` §Lifecycle
|
Land the OQ-12 resolution in code (`client.md` §Lifecycle
|
||||||
primitives): the primitives that make assembly-side reconnect/restore
|
primitives): the primitives that make assembly-side reconnect/restore
|
||||||
policy *possible* — and nothing more:
|
policy *possible*, and nothing more:
|
||||||
|
|
||||||
- **Explicitly cancellable handles** — every listener/handle the
|
- **Explicitly cancellable handles**, so every listener/handle the
|
||||||
client direction returns (the `-R` listener handles from
|
client direction returns (the `-R` listener handles from
|
||||||
`w4-reverse-listen`, the connection handle from `w4-dial`) carries
|
`w4-reverse-listen`, the connection handle from `w4-dial`) carries
|
||||||
an explicit cancellation/clean-close path; no handle relies on drop
|
an explicit cancellation/clean-close path; no handle relies on drop
|
||||||
timing alone for wire-visible state.
|
timing alone for wire-visible state.
|
||||||
- **Clean disconnect** — `Disconnect::ByApplication` (russh's client
|
- **Clean disconnect**, namely `Disconnect::ByApplication` (russh's client
|
||||||
`Handle::disconnect`): the explicit-close path leaves no half-open
|
`Handle::disconnect`): the explicit-close path leaves no half-open
|
||||||
channels, observed clean by the peer (row-pinned through the
|
channels, observed clean by the peer (row-pinned through the
|
||||||
server double).
|
server double).
|
||||||
- **No reconnect or restore policy owned by the crate** — the
|
- **No reconnect or restore policy owned by the crate**, hence the
|
||||||
module-doc posture: reconnect-and-restore (rebinding persisted
|
module-doc posture: reconnect-and-restore (rebinding persisted
|
||||||
tunnel ports — the alkminer flow, reverse-runner fact 4) is
|
tunnel ports, the alkminer flow, per reverse-runner fact 4) is
|
||||||
assembly policy; the crate provides bound ports, handles,
|
assembly policy; the crate provides bound ports, handles,
|
||||||
cancellation, clean close. The docs state the boundary explicitly
|
cancellation, clean close. The docs state the boundary explicitly
|
||||||
so no later task drifts into baking policy in.
|
so no later task drifts into baking policy in.
|
||||||
- **Contract rows through the server double** — cancel a listener
|
- **Contract rows through the server double**: cancel a listener
|
||||||
mid-connection (the server's forward is torn down, the bound port
|
mid-connection (the server's forward is torn down, the bound port
|
||||||
released); clean disconnect observed by the peer; the
|
released); clean disconnect observed by the peer; the
|
||||||
connection-handle cancellation path; the no-policy posture is a
|
connection-handle cancellation path; the no-policy posture is a
|
||||||
@@ -51,27 +51,27 @@ policy *possible* — and nothing more:
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- docs/architecture/client.md §Lifecycle primitives (the OQ-12
|
- docs/architecture/client.md §Lifecycle primitives (the OQ-12
|
||||||
resolution), docs/architecture/open-questions.md (OQ-12 — resolved:
|
resolution), docs/architecture/open-questions.md (OQ-12, which
|
||||||
primitives only)
|
resolved as primitives only)
|
||||||
- docs/research/reverse-runner.md (fact 4 — reconnect-and-restore is
|
- docs/research/reverse-runner.md (fact 4, where reconnect-and-restore is
|
||||||
assembly policy, kept in view so the crate never bakes it in)
|
assembly policy, kept in view so the crate never bakes it in)
|
||||||
- docs/research/russh.md §Client surface (`Handle::disconnect`, the
|
- docs/research/russh.md §Client surface (`Handle::disconnect`, the
|
||||||
forward cancel forms)
|
forward cancel forms)
|
||||||
- src/tunnel_map.rs (the server-side teardown posture — the cancel
|
- src/tunnel_map.rs (the server-side teardown posture, the cancel
|
||||||
semantics mirror)
|
semantics mirror)
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
### What landed (2026-10-29)
|
### What landed (2026-10-29)
|
||||||
|
|
||||||
- `src/client_accept.rs`: the **listener handles** —
|
- `src/client_accept.rs`: the **listener handles**, namely the
|
||||||
[`ReverseListener`] + [`ReverseEndpoint`] (the handles' surface the
|
[`ReverseListener`] + [`ReverseEndpoint`] (the handles' surface the
|
||||||
`w4-reverse-listen` block deferred to this task): `reverse_listen` /
|
`w4-reverse-listen` block deferred to this task): `reverse_listen` /
|
||||||
`reverse_listen_unix` return a handle carrying the *actually-bound*
|
`reverse_listen_unix` return a handle carrying the *actually-bound*
|
||||||
endpoint (the `bound_port()` accessor, `None` for the unix shape)
|
endpoint (the `bound_port()` accessor, `None` for the unix shape)
|
||||||
with the explicit `cancel()`; the shared cancel helpers
|
with the explicit `cancel()`; the shared cancel helpers
|
||||||
(`cancel_reverse_tcp` / `cancel_reverse_unix`) — both the handles'
|
(`cancel_reverse_tcp` / `cancel_reverse_unix`), and both the
|
||||||
`cancel()` and the stateless wire forms compose into the same
|
handles' `cancel()` and the stateless wire forms compose into the same
|
||||||
paths; the confirmed cancel releases the connection's listener
|
paths; the confirmed cancel releases the connection's listener
|
||||||
count exactly once (the repeat is the wire's unknown-listener
|
count exactly once (the repeat is the wire's unknown-listener
|
||||||
refusal, no release). The recorded decisions (module docs): no
|
refusal, no release). The recorded decisions (module docs): no
|
||||||
@@ -80,12 +80,12 @@ policy *possible* — and nothing more:
|
|||||||
its `cancel()` works against a live stack regardless of the
|
its `cancel()` works against a live stack regardless of the
|
||||||
connection handle's own holders; the stateless forms ride beside
|
connection handle's own holders; the stateless forms ride beside
|
||||||
1:1.
|
1:1.
|
||||||
- `src/client.rs`: the **`SshConnection` restructure** — the russh
|
- `src/client.rs`: the **`SshConnection` restructure**, where the russh
|
||||||
handle, limits/labels, acceptor registry and listener budget ride
|
handle, limits/labels, acceptor registry and listener budget ride
|
||||||
one `ConnectionShared` behind the `Arc` (the concurrency-safe
|
one `ConnectionShared` behind the `Arc` (the concurrency-safe
|
||||||
sharing the handles' independent `cancel()`s need; the russh client
|
sharing the handles' independent `cancel()`s need; the russh client
|
||||||
request methods all take `&self` at this pin, so the arc is the
|
request methods all take `&self` at this pin, so the arc is the
|
||||||
sharing shape — no mutex), `SshConnection`'s public surface
|
sharing shape, with no mutex), `SshConnection`'s public surface
|
||||||
unchanged (`close()` consumes the handle and sends
|
unchanged (`close()` consumes the handle and sends
|
||||||
`Disconnect::ByApplication`), and the module docs' new **§Lifecycle
|
`Disconnect::ByApplication`), and the module docs' new **§Lifecycle
|
||||||
posture** (the primitives-not-policy boundary, reverse-runner fact
|
posture** (the primitives-not-policy boundary, reverse-runner fact
|
||||||
@@ -98,16 +98,16 @@ policy *possible* — and nothing more:
|
|||||||
- `tests/client_dial.rs`: the every-build row
|
- `tests/client_dial.rs`: the every-build row
|
||||||
`dropping_the_connection_is_an_abrupt_teardown_close_is_the_only_clean_path`
|
`dropping_the_connection_is_an_abrupt_teardown_close_is_the_only_clean_path`
|
||||||
(the `next_session_outcome` observable: drop → the peer's session
|
(the `next_session_outcome` observable: drop → the peer's session
|
||||||
ends with an **error wire class, never the clean `Ok`** — the
|
ends with an **error wire class, never the clean `Ok`**, the
|
||||||
boundary pinned both ways: the clean `Ok` is only `close()`'s).
|
boundary pinned both ways: the clean `Ok` is only `close()`'s).
|
||||||
- `tests/client_reverse_listen.rs`: three new lifecycle rows —
|
- `tests/client_reverse_listen.rs`: three new lifecycle rows:
|
||||||
`dropping_the_listener_handle_leaves_no_drop_timing_wire_state`
|
`dropping_the_listener_handle_leaves_no_drop_timing_wire_state`
|
||||||
(drop → the peer's listen still serves a round-tripping data leg;
|
(drop → the peer's listen still serves a round-tripping data leg;
|
||||||
only the explicit cancel tears down),
|
only the explicit cancel tears down),
|
||||||
`the_listener_handle_cancel_tears_the_forward_down_and_releases_the_budget`
|
`the_listener_handle_cancel_tears_the_forward_down_and_releases_the_budget`
|
||||||
(ceiling → refuse pre-wire; the handle's cancel confirms and
|
(ceiling → refuse pre-wire; the handle's cancel confirms and
|
||||||
releases; the next listen admits; the repeat rides the wire and
|
releases; the next listen admits; the repeat rides the wire and
|
||||||
refuses — no budget manufactured),
|
refuses, with no budget manufactured),
|
||||||
`cancel_mid_data_leg_tears_the_forward_down_mid_connection` (the
|
`cancel_mid_data_leg_tears_the_forward_down_mid_connection` (the
|
||||||
cancel lands while a data leg is mid-pump; the forward tears down,
|
cancel lands while a data leg is mid-pump; the forward tears down,
|
||||||
the connection stays usable, ends clean); the existing rows
|
the connection stays usable, ends clean); the existing rows
|
||||||
@@ -117,46 +117,46 @@ policy *possible* — and nothing more:
|
|||||||
|
|
||||||
### Decided + recorded
|
### Decided + recorded
|
||||||
|
|
||||||
1. **The handles' shape — single `ReverseListener`, one
|
1. **The handles' shape, a single `ReverseListener`, one
|
||||||
`ReverseEndpoint` enum, not two handle types.** The endpoint enum
|
`ReverseEndpoint` enum, not two handle types.** The endpoint enum
|
||||||
mirrors the wire's two bind shapes (an actually-bound
|
mirrors the wire's two bind shapes (an actually-bound
|
||||||
address+port / a socket path); the tcp-flavored `bound_port()`
|
address+port / a socket path); the tcp-flavored `bound_port()`
|
||||||
accessor returns `Option` (the unix shape carries no port —
|
accessor returns `Option` (the unix shape carries no port,
|
||||||
honest, no panic path). A second handle type would have duplicated
|
honestly with no panic path). A second handle type would have duplicated
|
||||||
the cancel plumbing for a shape the cancel forms already treat as
|
the cancel plumbing for a shape the cancel forms already treat as
|
||||||
raw strings.
|
raw strings.
|
||||||
2. **The abrupt-drop fact (row-pinned, corrected mid-task):** at this
|
2. **The abrupt-drop fact (row-pinned, corrected mid-task):** at this
|
||||||
russh pin dropping the *last* wire-stack handle closes the
|
russh pin dropping the *last* wire-stack handle closes the
|
||||||
`Handle`'s sender; the client runner's `receiver.recv()` → `None`
|
`Handle`'s sender; the client runner's `receiver.recv()` → `None`
|
||||||
sets `disconnected` and the transport tears down abruptly — the
|
sets `disconnected` and the transport tears down abruptly; the
|
||||||
peer's session outcome is an error wire class (`Err(Disconnect)`
|
peer's session outcome is an error wire class (`Err(Disconnect)`
|
||||||
observed through the double). So the honest boundary is not
|
observed through the double). So the honest boundary is not
|
||||||
"drop does nothing" but **"drop is never the clean path"** — the
|
"drop does nothing" but **"drop is never the clean path"**, since the
|
||||||
clean `Ok` session end is only `close()`'s
|
clean `Ok` session end is only `close()`'s
|
||||||
(`Disconnect::ByApplication`), row-pinned in both directions
|
(`Disconnect::ByApplication`), row-pinned in both directions
|
||||||
(abrupt-on-drop, clean-on-close) through the server double. No
|
(abrupt-on-drop, clean-on-close) through the server double. No
|
||||||
handle relies on drop timing for *clean* wire state, and the
|
handle relies on drop timing for *clean* wire state, and the
|
||||||
listener handles' drop is a pure local no-op (no `Drop` hook; a
|
listener handles' drop is a pure local no-op (no `Drop` hook; a
|
||||||
cancellation is async and cannot hide in one — the listen lives at
|
cancellation is async and cannot hide in one, and the listen lives at
|
||||||
the peer until an explicit cancel or the connection's end).
|
the peer until an explicit cancel or the connection's end).
|
||||||
3. **The `SshConnection` restructure is a shape change, not a
|
3. **The `SshConnection` restructure is a shape change, not a
|
||||||
surface change** — the public surface (the wrapped methods,
|
surface change**: the public surface (the wrapped methods,
|
||||||
`limits()`/`address()`/`username()`/`is_closed()`/`acceptors()`/
|
`limits()`/`address()`/`username()`/`is_closed()`/`acceptors()`/
|
||||||
`close()`) is unchanged; only the internals moved behind the arc
|
`close()`) is unchanged; only the internals moved behind the arc
|
||||||
(the handles' independent cancel). No `Clone` was added — not in
|
(the handles' independent cancel). No `Clone` was added, which is not in
|
||||||
the contract, and the arc means a cloned handle's `close()`
|
the contract, and the arc means a cloned handle's `close()`
|
||||||
semantics would need their own contract; wave-5 review can add it
|
semantics would need their own contract; wave-5 review can add it
|
||||||
if an assembly shape needs it.
|
if an assembly shape needs it.
|
||||||
4. **A live `ReverseListener` keeps the wire stack alive after the
|
4. **A live `ReverseListener` keeps the wire stack alive after the
|
||||||
connection handle's drop** — the arc's ownership side effect,
|
connection handle's drop**, an arc ownership side effect recorded
|
||||||
recorded (the alkminer flow's shape: the listener handle outlives
|
(the alkminer flow's shape: the listener handle outlives
|
||||||
the connection slot; its `cancel()` composes on the still-live
|
the connection slot; its `cancel()` composes on the still-live
|
||||||
stack).
|
stack).
|
||||||
|
|
||||||
### Verification
|
### Verification
|
||||||
|
|
||||||
`cargo test` (default), `--features session`, `--features git`,
|
`cargo test` (default), `--features session`, `--features git`,
|
||||||
`--features tunnel`, `--all-features` — all green (16 test binaries
|
`--features tunnel`, `--all-features`: all green (16 test binaries
|
||||||
each); `cargo clippy --all-targets -- -D warnings` and
|
each); `cargo clippy --all-targets -- -D warnings` and
|
||||||
`cargo clippy --all-features --all-targets -- -D warnings` clean;
|
`cargo clippy --all-features --all-targets -- -D warnings` clean;
|
||||||
`cargo fmt --check` clean; `cargo doc --no-deps` warning-free in the
|
`cargo fmt --check` clean; `cargo doc --no-deps` warning-free in the
|
||||||
@@ -179,20 +179,20 @@ Details in the `Landed` block below.
|
|||||||
|
|
||||||
Landed (`feat(client)`: w4-lifecycle):
|
Landed (`feat(client)`: w4-lifecycle):
|
||||||
|
|
||||||
- **`src/client_accept.rs`** — the listener handles: `ReverseListener`
|
- **`src/client_accept.rs`**, the listener handles: `ReverseListener`
|
||||||
(+ `ReverseEndpoint`) — `reverse_listen` / `reverse_listen_unix`
|
(+ `ReverseEndpoint`), which `reverse_listen` / `reverse_listen_unix`
|
||||||
now return them; the actually-bound endpoint carried, the explicit
|
now return; the actually-bound endpoint carried, the explicit
|
||||||
`cancel()` the wire-honest teardown, the listener count released
|
`cancel()` the wire-honest teardown, the listener count released
|
||||||
exactly once per confirmed cancel through the shared cancel paths
|
exactly once per confirmed cancel through the shared cancel paths
|
||||||
the stateless wire forms compose into too; no `Drop` hides a wire
|
the stateless wire forms compose into too; no `Drop` hides a wire
|
||||||
op (recorded, row-pinned no-drop-timing wire state).
|
op (recorded, row-pinned no-drop-timing wire state).
|
||||||
- **`src/client.rs`** — the `SshConnection` internals behind the arc
|
- **`src/client.rs`**, the `SshConnection` internals behind the arc
|
||||||
(`ConnectionShared`) + the §Lifecycle posture module docs (the
|
(`ConnectionShared`) + the §Lifecycle posture module docs (the
|
||||||
primitives-not-policy boundary, the abrupt-drop/clean-close facts);
|
primitives-not-policy boundary, the abrupt-drop/clean-close facts);
|
||||||
the public surface unchanged (`close()` is the clean path).
|
the public surface unchanged (`close()` is the clean path).
|
||||||
- **`src/lib.rs`** — the handle exports; **`docs/architecture/
|
- **`src/lib.rs`** for the handle exports; **`docs/architecture/
|
||||||
client.md`** — the §Lifecycle primitives landing block.
|
client.md`** for the §Lifecycle primitives landing block.
|
||||||
- **Rows** — 3 new tunnel-build rows through the server double
|
- **Rows**, with 3 new tunnel-build rows through the server double
|
||||||
(`tests/client_reverse_listen.rs`: the drop-handle no-wire-state
|
(`tests/client_reverse_listen.rs`: the drop-handle no-wire-state
|
||||||
row, the handle-cancel + budget-release row, the cancel-mid-data-leg
|
row, the handle-cancel + budget-release row, the cancel-mid-data-leg
|
||||||
row) + 1 new every-build row (`tests/client_dial.rs`: the abrupt-
|
row) + 1 new every-build row (`tests/client_dial.rs`: the abrupt-
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w4-op-rows
|
id: w4-op-rows
|
||||||
name: Wave 4 — the client op rows (exec/pty, direct-tcpip, git exec, sftp decline)
|
name: "Wave 4: the client op rows (exec/pty, direct-tcpip, git exec, sftp decline)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w4-dial]
|
depends_on: [w4-dial]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -11,54 +11,54 @@ level: implementation
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Land the op rows — family op → vanilla translation per
|
Land the op rows: family op → vanilla translation per
|
||||||
`client.md`'s table, pointed upstream at a vanilla server (the
|
`client.md`'s table, pointed upstream at a vanilla server (the
|
||||||
reverse-runner shape: the hub drives every channel open). Type
|
reverse-runner shape: the hub drives every channel open). Type
|
||||||
symmetry at the seam is the contract: family-protocol types in,
|
symmetry at the seam is the contract: family-protocol types in,
|
||||||
vanilla wire ops out, no vanilla-concept leaks up. The POC-3 §D.3
|
vanilla wire ops out, no vanilla-concept leaks up. The POC-3 §D.3
|
||||||
mechanics are the body of knowledge this task lands in code:
|
mechanics are the body of knowledge this task lands in code:
|
||||||
|
|
||||||
- **Session exec/pty rows** (under `session`) — `channel_open_session`
|
- **Session exec/pty rows** (under `session`): `channel_open_session`
|
||||||
+ the exec/pty-req/shell-req translations onto the alktty session
|
+ the exec/pty-req/shell-req translations onto the alktty session
|
||||||
types (pipe vs PTY; exit semantics): `Channel::exec()` — **no
|
types (pipe vs PTY; exit semantics): `Channel::exec()`, with **no
|
||||||
`request_exec` at this pin**; Success/Failure arrive on the channel
|
`request_exec` at this pin**; Success/Failure arrive on the channel
|
||||||
queue via `wait()`-drain, **not** as `Err` from the request call
|
queue via `wait()`-drain, **not** as `Err` from the request call
|
||||||
(drain until the expected message, `continue` past the rest);
|
(drain until the expected message, `continue` past the rest);
|
||||||
`window_change`/`signal` are fire-and-forget and must be sent
|
`window_change`/`signal` are fire-and-forget and must be sent
|
||||||
**before** `into_stream()` (the Channel is consumed by conversion);
|
**before** `into_stream()` (the Channel is consumed by conversion);
|
||||||
**never consume the channel queue before `into_stream()`** (a
|
**never consume the channel queue before `into_stream()`**, because a
|
||||||
`wait()` on a fresh channel breaks it — POC-1 §6, both sides). The
|
`wait()` on a fresh channel breaks it (POC-1 §6, both sides). The
|
||||||
data pump + the exit consumption mirror the server direction's
|
data pump + the exit consumption mirror the server direction's
|
||||||
derivation contract (the pipe-mode pid-only signal caveat rides the
|
derivation contract (the pipe-mode pid-only signal caveat rides the
|
||||||
alktty exit-chunk semantics — `mapping.md`'s contract, consumed not
|
alktty exit-chunk semantics, `mapping.md`'s contract, consumed not
|
||||||
re-derived).
|
re-derived).
|
||||||
- **`direct-tcpip` typed dial** (under `tunnel`) — the row the `-L`
|
- **`direct-tcpip` typed dial** (under `tunnel`), the row the `-L`
|
||||||
leg and the alksocks `DialFn` compose over: `channel_open_direct_tcpip`
|
leg and the alksocks `DialFn` compose over: `channel_open_direct_tcpip`
|
||||||
resolves to `Err(ChannelOpenFailure)` on server reject (POC-1 §6) —
|
resolves to `Err(ChannelOpenFailure)` on server reject (POC-1 §6),
|
||||||
the typed open, no `wait()` before `into_stream`; **the connection
|
giving the typed open with no `wait()` before `into_stream`; **the connection
|
||||||
stays usable after a declined open** (pinned by row). The
|
stays usable after a declined open** (pinned by row). The
|
||||||
`direct-streamlocal` twin with the runtime-Unix gate (OQ-11
|
`direct-streamlocal` twin with the runtime-Unix gate (OQ-11
|
||||||
symmetry: universal compile, clean off-Unix decline).
|
symmetry: universal compile, clean off-Unix decline).
|
||||||
- **Git exec consumption** (under `git`) — the same exec grammar the
|
- **Git exec consumption** (under `git`), the same exec grammar the
|
||||||
server side parses (`git-{upload,receive}-pack '/repo'`),
|
server side parses (`git-{upload,receive}-pack '/repo'`),
|
||||||
**reused, not forked** — pointed upstream at a vanilla `sshd`:
|
**reused, not forked**, pointed upstream at a vanilla `sshd`:
|
||||||
parse → exec → the pack duplex pumped → exit-status consumed. The
|
parse → exec → the pack duplex pumped → exit-status consumed. The
|
||||||
per-length limits and service-only decline classes ride the shared
|
per-length limits and service-only decline classes ride the shared
|
||||||
grammar's verdicts.
|
grammar's verdicts.
|
||||||
- **The sftp subsystem placeholder decline** (every build, OQ-13) —
|
- **The sftp subsystem placeholder decline** (every build, OQ-13),
|
||||||
the client-side half of the tested decline placeholder: a
|
the client-side half of the tested decline placeholder: a
|
||||||
`subsystem "sftp"` open declines cleanly in every build (the
|
`subsystem "sftp"` open declines cleanly in every build (the
|
||||||
feature exists, the route doesn't until alkfs lands). The
|
feature exists, the route doesn't until alkfs lands). The
|
||||||
server-side rows already landed in wave 2; this lands the client
|
server-side rows already landed in wave 2; this lands the client
|
||||||
mirror so both directions are contract-tested.
|
mirror so both directions are contract-tested.
|
||||||
- **Contract rows through the server double, per build** — the
|
- **Contract rows through the server double, per build**, with the
|
||||||
`w4-dial` fake-`sshd` vehicle grown with the op-row surfaces:
|
`w4-dial` fake-`sshd` vehicle grown with the op-row surfaces:
|
||||||
featured + working rows (exec exit-status, pty window-change
|
featured + working rows (exec exit-status, pty window-change
|
||||||
ordering, direct-tcpip pump, git exec round trip), decline rows
|
ordering, direct-tcpip pump, git exec round trip), decline rows
|
||||||
(server-rejected opens, the sftp decline, off-Unix streamlocal),
|
(server-rejected opens, the sftp decline, off-Unix streamlocal),
|
||||||
the connection-stays-usable row after each decline class. The
|
the connection-stays-usable row after each decline class. The
|
||||||
real-tool smoke posture decided + recorded (a real `sshd` exec is
|
real-tool smoke posture decided + recorded (a real `sshd` exec is
|
||||||
cheap where tooling permits — check-gated like the wave-3 smokes;
|
cheap where tooling permits, check-gated like the wave-3 smokes;
|
||||||
else named wave-5 e2e territory explicitly).
|
else named wave-5 e2e territory explicitly).
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
@@ -83,15 +83,15 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
|
|
||||||
- docs/architecture/client.md §Op rows (the table), §Correctness
|
- docs/architecture/client.md §Op rows (the table), §Correctness
|
||||||
facts, §What the crate does *not* include (job supervision is
|
facts, §What the crate does *not* include (job supervision is
|
||||||
assembly work — the crate gives channels + exit semantics)
|
assembly work; the crate gives channels + exit semantics)
|
||||||
- docs/architecture/mapping.md §The exit derivation contract (the
|
- docs/architecture/mapping.md §The exit derivation contract (the
|
||||||
mirror the client consumes), docs/architecture/decisions/003-feature-graph.md
|
mirror the client consumes), docs/architecture/decisions/003-feature-graph.md
|
||||||
(the feature rows), docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
(the feature rows), docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
||||||
(exec-only git, both directions)
|
(exec-only git, both directions)
|
||||||
- docs/research/poc-3-mapping-bakeoff.md §D.3 (the client mechanics —
|
- docs/research/poc-3-mapping-bakeoff.md §D.3 (the client mechanics,
|
||||||
the body of knowledge), docs/research/poc-1-auth-bridge.md §6 (the
|
the body of knowledge), docs/research/poc-1-auth-bridge.md §6 (the
|
||||||
open-call `Err` semantics), docs/research/reverse-runner.md (the
|
open-call `Err` semantics), docs/research/reverse-runner.md (the
|
||||||
per-connection surface table — the consumer evidence)
|
per-connection surface table, the consumer evidence)
|
||||||
- src/git_map.rs (the grammar reused), src/tty_map.rs (the exit-chunk
|
- src/git_map.rs (the grammar reused), src/tty_map.rs (the exit-chunk
|
||||||
semantics consumed), russh 0.65 `client::Handle` (`channel_open_session`
|
semantics consumed), russh 0.65 `client::Handle` (`channel_open_session`
|
||||||
/ `channel_open_direct_tcpip` / `channel_open_direct_streamlocal`)
|
/ `channel_open_direct_tcpip` / `channel_open_direct_streamlocal`)
|
||||||
@@ -101,7 +101,8 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
### What landed (2026-10-29)
|
### What landed (2026-10-29)
|
||||||
|
|
||||||
- `src/client_ops.rs` (new, every build; rows feature-gated inside):
|
- `src/client_ops.rs` (new, every build; rows feature-gated inside):
|
||||||
the op rows — the family op → vanilla translation surfaces, all on
|
the op rows, the family op → vanilla translation surfaces, all
|
||||||
|
on
|
||||||
`SshConnection` (`dial`'s handle; sibling-module method impls over
|
`SshConnection` (`dial`'s handle; sibling-module method impls over
|
||||||
the crate accessors `russh_handle()` / `budgets()`):
|
the crate accessors `russh_handle()` / `budgets()`):
|
||||||
- *session*: `exec` (pipe), `pty_exec` (`ssh -t`), `shell(Option<
|
- *session*: `exec` (pipe), `pty_exec` (`ssh -t`), `shell(Option<
|
||||||
@@ -110,30 +111,30 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
parity) → `RemoteSession`, the **family chunk plane** (§below).
|
parity) → `RemoteSession`, the **family chunk plane** (§below).
|
||||||
- *tunnel*: `tunnel_direct(host, port)` /
|
- *tunnel*: `tunnel_direct(host, port)` /
|
||||||
`tunnel_direct_unix(path)` → `TunnelChannel` (a plain byte
|
`tunnel_direct_unix(path)` → `TunnelChannel` (a plain byte
|
||||||
duplex — the alksocks `DialFn` composes it; the `-D` leg's
|
duplex, which the alksocks `DialFn` composes; the `-D` leg's
|
||||||
composition needs no dependency, ADR-003 §1a).
|
composition needs no dependency, ADR-003 §1a).
|
||||||
- *git*: `git_service(alkgit::Service, repo_id)` →
|
- *git*: `git_service(alkgit::Service, repo_id)` →
|
||||||
`GitPackSession` (raw pack duplex + stderr duplex + the
|
`GitPackSession` (raw pack duplex + stderr duplex + the
|
||||||
consumed-exit oneshot).
|
consumed-exit oneshot).
|
||||||
- *every build*: `sftp()` → `Err(OpError::SftpNotServed)` (the
|
- *every build*: `sftp()` → `Err(OpError::SftpNotServed)` (the
|
||||||
`Ok` type is `Infallible` — the feature ships but the route
|
`Ok` type is `Infallible`, so the feature ships but the route
|
||||||
doesn't, OQ-13, now tested in both the featureless and the
|
doesn't, OQ-13, now tested in both the featureless and the
|
||||||
featured build).
|
featured build).
|
||||||
- `src/exit_code.rs` (new, extracted from `tty_map.rs` — the wave's
|
- `src/exit_code.rs` (new, extracted from `tty_map.rs`, the wave's
|
||||||
structural edit): the derivation contract's machinery
|
structural edit): the derivation contract's machinery
|
||||||
(`SIGNAL_NAMES` / `ExitReport` / `derive_exit` /
|
(`SIGNAL_NAMES` / `ExitReport` / `derive_exit` /
|
||||||
`sig_from_name` / `sig_control_name`) is now **two-direction
|
`sig_from_name` / `sig_control_name`) is now **two-direction
|
||||||
territory** — the module gained the client arm's
|
territory**; the module gained the client arm's
|
||||||
`signal_code_from_name` (the wire `exit-signal` name → the family
|
`signal_code_from_name` (the wire `exit-signal` name → the family
|
||||||
`−signum` code, `−1` for the unmapped/the sentinel collision) and
|
`−signum` code, `−1` for the unmapped/the sentinel collision) and
|
||||||
the module docs carry the client arm's rules as the same
|
the module docs carry the client arm's rules as the same
|
||||||
contract's inverse. `tty_map.rs` consumes it (no fork anywhere);
|
contract's inverse. `tty_map.rs` consumes it (no fork anywhere);
|
||||||
`handler.rs`'s signal row follows the moved path.
|
`handler.rs`'s signal row follows the moved path.
|
||||||
- `src/error.rs`: [`OpError`] — the op rows' closed taxonomy
|
- `src/error.rs`: [`OpError`], the op rows' closed taxonomy
|
||||||
(`OpenDecline(OpenDeclineReason)` — the *family-mirrored* copy of
|
(`OpenDecline(OpenDeclineReason)`, the *family-mirrored* copy of
|
||||||
the typed open's wire class,
|
the typed open's wire class,
|
||||||
`Other{code,reason}` verbatim, nothing collapsed /
|
`Other{code,reason}` verbatim, nothing collapsed /
|
||||||
`RequestRefused` (the `wait()`-drain's `Failure` reply — POC-3
|
`RequestRefused` (the `wait()`-drain's `Failure` reply, as POC-3
|
||||||
§D.3's not-an-`Err` fact surfaced as the op's decline class) /
|
§D.3's not-an-`Err` fact surfaced as the op's decline class) /
|
||||||
`ChannelEnded` (the drain saw the channel's end; nothing
|
`ChannelEnded` (the drain saw the channel's end; nothing
|
||||||
fabricated) / `CommandTooLong` (the exec-length budget's
|
fabricated) / `CommandTooLong` (the exec-length budget's
|
||||||
@@ -141,14 +142,14 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
validation gate mirrored, pre-wire) / `UnixUnavailable` (the OQ-11
|
validation gate mirrored, pre-wire) / `UnixUnavailable` (the OQ-11
|
||||||
gate, pre-wire) / `SftpNotServed` (the placeholder) /
|
gate, pre-wire) / `SftpNotServed` (the placeholder) /
|
||||||
`Wire(russh::Error)` (transparent); the `From<russh::Error>` maps
|
`Wire(russh::Error)` (transparent); the `From<russh::Error>` maps
|
||||||
the typed open's `ChannelOpenFailure` into the family mirror —
|
the typed open's `ChannelOpenFailure` into the family mirror,
|
||||||
one conversion point, unit rows pin every class).
|
with one conversion point, unit rows pin every class).
|
||||||
- `src/dispatch.rs`: the shared grammar's **construction arm** —
|
- `src/dispatch.rs`: the shared grammar's **construction arm**,
|
||||||
`build_git_exec_command(route, repo_id)` beside `parse_exec`, with
|
`build_git_exec_command(route, repo_id)` beside `parse_exec`, with
|
||||||
unit rows proving the construction round-trips the parse for both
|
unit rows proving the construction round-trips the parse for both
|
||||||
services (the grammar's gate is the `Service` *type*: the
|
services (the grammar's gate is the `Service` *type*: the
|
||||||
construction arm can emit no other token, so the client rows are
|
construction arm can emit no other token, so the client rows are
|
||||||
structurally unreachable by `unknown-service`/`service-only` —
|
structurally unreachable by `unknown-service`/`service-only`,
|
||||||
recorded).
|
recorded).
|
||||||
- `src/client.rs`: the crate accessors (`russh_handle`/`budgets`,
|
- `src/client.rs`: the crate accessors (`russh_handle`/`budgets`,
|
||||||
feature-gated; no public-surface change); `src/lib.rs`: the
|
feature-gated; no public-surface change); `src/lib.rs`: the
|
||||||
@@ -156,37 +157,37 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
(`RemoteSession` / `TunnelChannel` / `GitPackSession`,
|
(`RemoteSession` / `TunnelChannel` / `GitPackSession`,
|
||||||
`OpError`/`OpenDeclineReason` public).
|
`OpError`/`OpenDeclineReason` public).
|
||||||
- `tests/common/sshd_double.rs`: the fake `sshd` grown with the
|
- `tests/common/sshd_double.rs`: the fake `sshd` grown with the
|
||||||
op-row script — the session requests' reply knob, the scripted
|
op-row script, with the session requests' reply knob, the scripted
|
||||||
program (stdout/stderr/exit — `exit-then-close`, the vanilla
|
program (stdout/stderr/exit in the `exit-then-close` vanilla
|
||||||
discipline; a no-output/no-exit script launches *nothing*, keeping
|
discipline; a no-output/no-exit script launches *nothing*, keeping
|
||||||
interactive sessions open), the direct opens' echo-or-reject
|
interactive sessions open), the direct opens' echo-or-reject
|
||||||
behavior, a counted-refusal knob for the
|
behavior, a counted-refusal knob for the
|
||||||
stays-usable-after-a-decline rows, and the observation surface
|
stays-usable-after-a-decline rows, and the observation surface
|
||||||
(`SshdObserved`: recorded execs/ptys/windows/signals — the wire
|
(`SshdObserved`: recorded execs/ptys/windows/signals, the wire
|
||||||
translations' witnesses); `tests/client_ops.rs`: the contract rows
|
translations' witnesses); `tests/client_ops.rs`: the contract rows
|
||||||
(below).
|
(below).
|
||||||
|
|
||||||
### Decided + recorded
|
### Decided + recorded
|
||||||
|
|
||||||
1. **The family plane (the session rows) is the alktty chunk framing
|
1. **The family plane (the session rows) is the alktty chunk framing
|
||||||
in both directions** — the exact plane the server direction's
|
in both directions**, the exact plane the server direction's
|
||||||
bridge speaks, consumed not re-derived: stdin chunks + `CTRL_IN`
|
bridge speaks, consumed not re-derived: stdin chunks + `CTRL_IN`
|
||||||
(sentinel/`Resize`/`Signal`/`Eof`) toward the wire; stdout/stderr
|
(sentinel/`Resize`/`Signal`/`Eof`) toward the wire; stdout/stderr
|
||||||
chunks + the **in-band exit chunk, last** toward the hub. The
|
chunks + the **in-band exit chunk, last** toward the hub. The
|
||||||
exit ride is the family exit-currency (`ControlMessage::Exit`'s
|
exit ride is the family exit-currency (`ControlMessage::Exit`'s
|
||||||
`i32`): wire `ExitStatus(N)` → `N`, wire `ExitSignal(name)` →
|
`i32`): wire `ExitStatus(N)` → `N`, wire `ExitSignal(name)` →
|
||||||
`−signum` via the table's inverse read (unmapped names ride the
|
`−signum` via the table's inverse read (unmapped names ride the
|
||||||
`−1` sentinel — including a genuine SIGHUP, the same collision
|
`−1` sentinel, including a genuine SIGHUP, the same collision
|
||||||
rule 3 consumes server-side). A session ending with no report
|
rule 3 consumes server-side). A session ending with no report
|
||||||
delivers no exit chunk (the no-exit-chunk close family's mirror).
|
delivers no exit chunk (the no-exit-chunk close family's mirror).
|
||||||
2. **The exec/pty/git rows never `into_stream()`; the tunnel rows
|
2. **The exec/pty/git rows never `into_stream()`; the tunnel rows
|
||||||
never wait.** The mechanics facts pin the *shapes*: the
|
never wait.** The mechanics facts pin the *shapes*: the
|
||||||
conversion's reader swallows exactly the non-`Data` messages the
|
conversion's reader swallows exactly the non-`Data` messages the
|
||||||
exit reports ride on, so an exit-consuming row is structurally a
|
exit reports ride on, so an exit-consuming row is structurally a
|
||||||
**split-halves wait-pump** (`Channel::split()` + spawned drain of
|
**split-halves wait-pump** (`Channel::split()` plus a spawned drain
|
||||||
`wait()` — the same shape `w4-git-exit-close` pinned server-side;
|
of `wait()`, the same shape `w4-git-exit-close` pinned server-side);
|
||||||
the write half stays alive, so the fire-and-forget
|
the write half stays alive, so the fire-and-forget
|
||||||
`window_change`/`signal` rows ride mid-session — the ordering
|
`window_change`/`signal` rows ride mid-session; the ordering
|
||||||
fact's constraint, that controls precede `into_stream()`, is
|
fact's constraint, that controls precede `into_stream()`, is
|
||||||
respected by *construction* across both row kinds). The tunnel
|
respected by *construction* across both row kinds). The tunnel
|
||||||
rows go typed-open → straight `into_stream()`, no queue
|
rows go typed-open → straight `into_stream()`, no queue
|
||||||
@@ -197,7 +198,7 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
disambiguation** (`ChunkReader::read_chunk`, not
|
disambiguation** (`ChunkReader::read_chunk`, not
|
||||||
`tty_map::read_bridge_frame`): that plane's chunk types are
|
`tty_map::read_bridge_frame`): that plane's chunk types are
|
||||||
stdin `0` / `CTRL_IN` `3`; a negotiation-error frame never exists
|
stdin `0` / `CTRL_IN` `3`; a negotiation-error frame never exists
|
||||||
toward the door — and a `0x00`-leading read there is a legitimately
|
toward the door, and a `0x00`-leading read there is a legitimately
|
||||||
valid zero-length stdin chunk, so `read_bridge_frame`'s
|
valid zero-length stdin chunk, so `read_bridge_frame`'s
|
||||||
`0x00`-disambiguation (whose home is the alktty→hub direction,
|
`0x00`-disambiguation (whose home is the alktty→hub direction,
|
||||||
where error frames live) would misparse it. The wave-3 harness's
|
where error frames live) would misparse it. The wave-3 harness's
|
||||||
@@ -211,22 +212,22 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
write failure); the *input* pump closes the wire channel on it.
|
write failure); the *input* pump closes the wire channel on it.
|
||||||
stdin-EOF (the sentinel, a `CTRL_IN` Eof, or the hub's input half
|
stdin-EOF (the sentinel, a `CTRL_IN` Eof, or the hub's input half
|
||||||
dropping) sends the wire EOF *once* and parks the pump in the
|
dropping) sends the wire EOF *once* and parks the pump in the
|
||||||
idle-for-kill state (the session keeps pumping — stdin closure
|
idle-for-kill state (the session keeps pumping; stdin closure
|
||||||
never ends the session). Dropping the surface (both halves) closes
|
never ends the session). Dropping the surface (both halves) closes
|
||||||
the channel — wire-clean, nothing dangling outside the dial's
|
the channel, wire-clean, nothing dangling outside the dial's
|
||||||
own wall clock.
|
own wall clock.
|
||||||
5. **The launch drains are message-capped** (`DRAIN_MESSAGE_CAP`,
|
5. **The launch drains are message-capped** (`DRAIN_MESSAGE_CAP`,
|
||||||
1024): a peer answering only with non-reply messages cannot loop
|
1024): a peer answering only with non-reply messages cannot loop
|
||||||
the drain unboundedly (bounded resources; the drain's idle waits
|
the drain unboundedly (bounded resources; the drain's idle waits
|
||||||
are bounded by the dial's always-set inactivity timeout). A
|
are bounded by the dial's always-set inactivity timeout). A
|
||||||
refusal closes the channel behind itself (`drain_launch_request`
|
refusal closes the channel behind itself (`drain_launch_request`,
|
||||||
— the row owned it; never half-serves).
|
which the row owned; never half-serves).
|
||||||
6. **The pty translation**: family `TerminalParamsWire` → wire
|
6. **The pty translation**: family `TerminalParamsWire` → wire
|
||||||
`pty_request` verbatim (term/cols/rows/pixels, `term: None` → the
|
`pty_request` verbatim (term/cols/rows/pixels, `term: None` → the
|
||||||
empty-terminal-string value; **the modes JSON never rides the
|
empty-terminal-string value; **the modes JSON never rides the
|
||||||
wire** — always the empty set, the OQ-44 reserved posture
|
wire**, always the empty set per the OQ-44 reserved posture
|
||||||
mirrored: the door never invents wire terminal modes); a refused
|
mirrored: the door never invents wire terminal modes); a refused
|
||||||
pty request refuses the whole row (never a pty-less fall) — the
|
pty request refuses the whole row (never a pty-less fall), the
|
||||||
never-half-capability row.
|
never-half-capability row.
|
||||||
7. **The git row's gates before any wire op** (the shared grammar's
|
7. **The git row's gates before any wire op** (the shared grammar's
|
||||||
verdicts): `validate_repo_id` → `InvalidRepoId`; the built
|
verdicts): `validate_repo_id` → `InvalidRepoId`; the built
|
||||||
@@ -236,7 +237,7 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
stderr rides a split raw duplex (the hub may drop it); the pack
|
stderr rides a split raw duplex (the hub may drop it); the pack
|
||||||
duplex is the protocol's own byte plane, untouched.
|
duplex is the protocol's own byte plane, untouched.
|
||||||
8. **The tunnel rows' originator**: the door's label
|
8. **The tunnel rows' originator**: the door's label
|
||||||
(`"alk-ssh"`, port 0) — nothing invented, nothing leaked; peer
|
(`"alk-ssh"`, port 0): nothing invented, nothing leaked; peer
|
||||||
logs name the door (recorded; a real originator would be the
|
logs name the door (recorded; a real originator would be the
|
||||||
assembly's local-listener fact, and the `DialFn` shape carries
|
assembly's local-listener fact, and the `DialFn` shape carries
|
||||||
the target only).
|
the target only).
|
||||||
@@ -246,7 +247,7 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
this direction: the always-on wall clock/keepalives, the russh
|
this direction: the always-on wall clock/keepalives, the russh
|
||||||
windows, the bounded duplexes/mpscs, the drain cap.
|
windows, the bounded duplexes/mpscs, the drain cap.
|
||||||
10. **No crate events on the client op rows**: the outcomes ride the
|
10. **No crate events on the client op rows**: the outcomes ride the
|
||||||
typed surfaces (`Result`/channels) — the decline* event
|
typed surfaces (`Result`/channels), as the decline* event
|
||||||
vocabulary takes a wire channel id, and a *client-side* declined
|
vocabulary takes a wire channel id, and a *client-side* declined
|
||||||
open has none to name (the typed open carries no id back);
|
open has none to name (the typed open carries no id back);
|
||||||
the events remain the server-direction (and dial-skeleton) rows'
|
the events remain the server-direction (and dial-skeleton) rows'
|
||||||
@@ -255,18 +256,18 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
11. **The smoke posture** (decided + recorded, per the task's item):
|
11. **The smoke posture** (decided + recorded, per the task's item):
|
||||||
the session rows' cheap real-stack smoke is the
|
the session rows' cheap real-stack smoke is the
|
||||||
alkssh-client-op-row ↔ alkssh-server loopback (the client exec
|
alkssh-client-op-row ↔ alkssh-server loopback (the client exec
|
||||||
row against the server direction's *real* session rows — both
|
row against the server direction's *real* session rows, with both
|
||||||
chunk-bridge arms over one live wire), landed as the row
|
chunk-bridge arms over one live wire), landed as the row
|
||||||
`smoke_client_exec_row_against_the_server_directions_session_rows`
|
`smoke_client_exec_row_against_the_server_directions_session_rows`
|
||||||
(gated on `sh` like the wave-3 smokes, skipped-with-reason when
|
(gated on `sh` like the wave-3 smokes, skipped-with-reason when
|
||||||
absent). A **real-vanilla-`sshd`** dial smoke is wave-5 e2e
|
absent). A **real-vanilla-`sshd`** dial smoke is wave-5 e2e
|
||||||
territory explicitly — nothing silently dropped.
|
territory explicitly, with nothing silently dropped.
|
||||||
|
|
||||||
### Test rows (`tests/client_ops.rs`, one per decline/contract class; per-build binaries)
|
### Test rows (`tests/client_ops.rs`, one per decline/contract class; per-build binaries)
|
||||||
|
|
||||||
- *every build*: the sftp placeholder's decline (before any wire op,
|
- *every build*: the sftp placeholder's decline (before any wire op,
|
||||||
no channel opened at the peer) + the featured-build variant (under
|
no channel opened at the peer) + the featured-build variant (under
|
||||||
`sftp`) — the connection-stays-usable close on both.
|
`sftp`), and the connection-stays-usable close on both.
|
||||||
- *session* (10 rows + the smoke): the exec family-plane row
|
- *session* (10 rows + the smoke): the exec family-plane row
|
||||||
(command rides the wire verbatim; stdout/stderr chunks distinct;
|
(command rides the wire verbatim; stdout/stderr chunks distinct;
|
||||||
the exit chunk in-band, last; the stdin sentinel = wire EOF), the
|
the exit chunk in-band, last; the stdin sentinel = wire EOF), the
|
||||||
@@ -277,7 +278,7 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
rows (CTRL_IN Resize → wire `window_change`; Signal → wire
|
rows (CTRL_IN Resize → wire `window_change`; Signal → wire
|
||||||
`signal`, name verbatim), the exit-signal mirror row (terminal
|
`signal`, name verbatim), the exit-signal mirror row (terminal
|
||||||
`exit-signal("TERM")` → the family code `−15` in-band), the
|
`exit-signal("TERM")` → the family code `−15` in-band), the
|
||||||
never-half-capability row (a refused pty refuses the whole row —
|
never-half-capability row (a refused pty refuses the whole row, so
|
||||||
no exec behind it ever rode the wire), the server-rejected-open
|
no exec behind it ever rode the wire), the server-rejected-open
|
||||||
decline row (`OpenDeclined(Prohibited)`) with the
|
decline row (`OpenDeclined(Prohibited)`) with the
|
||||||
stays-usable-after-the-decline launch on the same connection, and
|
stays-usable-after-the-decline launch on the same connection, and
|
||||||
@@ -286,8 +287,8 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
round-trips; `poll_shutdown` EOF rides), the
|
round-trips; `poll_shutdown` EOF rides), the
|
||||||
reject-maps-the-class + stays-usable row (`ConnectFailed` mirror;
|
reject-maps-the-class + stays-usable row (`ConnectFailed` mirror;
|
||||||
the streamlocal twin then opens on the same connection on-Unix;
|
the streamlocal twin then opens on the same connection on-Unix;
|
||||||
`#[cfg(not(unix))]`'s OQ-11 decline row compiles off-Unix only —
|
`#[cfg(not(unix))]`'s OQ-11 decline row compiles off-Unix only, per the
|
||||||
the universal-compile/clean-decline gate mirrored from the server
|
universal-compile/clean-decline gate mirrored from the server
|
||||||
side), the twin's on-Unix pump served in the same row.
|
side), the twin's on-Unix pump served in the same row.
|
||||||
- *git*: the grammar's construction riding the wire verbatim + the
|
- *git*: the grammar's construction riding the wire verbatim + the
|
||||||
pack duplex pump + the `exit-status(0)` consumed into the family
|
pack duplex pump + the `exit-status(0)` consumed into the family
|
||||||
@@ -300,7 +301,7 @@ mechanics are the body of knowledge this task lands in code:
|
|||||||
### Verification
|
### Verification
|
||||||
|
|
||||||
`cargo test` (default), per-feature (`--features {session, git,
|
`cargo test` (default), per-feature (`--features {session, git,
|
||||||
tunnel, sftp}`), pairs, and `--all-features` — all 15 test binaries
|
tunnel, sftp}`), pairs, and `--all-features`, all 15 test binaries
|
||||||
green in the full builds (the client_ops binary: 18 rows under
|
green in the full builds (the client_ops binary: 18 rows under
|
||||||
`--all-features`, the per-build sets: session 12 / git 5 / tunnel 3
|
`--all-features`, the per-build sets: session 12 / git 5 / tunnel 3
|
||||||
/ featureless 1); `cargo clippy --all-features --all-targets` and
|
/ featureless 1); `cargo clippy --all-features --all-targets` and
|
||||||
@@ -316,7 +317,7 @@ typed-open decline's one-to-one mirror). The session rows give the
|
|||||||
hub the alktty chunk plane in both directions with the exit
|
hub the alktty chunk plane in both directions with the exit
|
||||||
consumed in-band as the family exit chunk (the derivation contract's
|
consumed in-band as the family exit chunk (the derivation contract's
|
||||||
client arm, extracted into the shared two-direction `exit_code.rs`
|
client arm, extracted into the shared two-direction `exit_code.rs`
|
||||||
module — table and rules never forked); the tunnel rows are the
|
module; table and rules never forked there); the tunnel rows are the
|
||||||
typed `direct-tcpip`/`direct-streamlocal` opens into raw byte
|
typed `direct-tcpip`/`direct-streamlocal` opens into raw byte
|
||||||
duplexes (rejects mirror to `OpenDeclineReason`, the connection
|
duplexes (rejects mirror to `OpenDeclineReason`, the connection
|
||||||
stays usable, row-pinned per decline class); the git row builds the
|
stays usable, row-pinned per decline class); the git row builds the
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
id: w4-reverse-listen
|
id: w4-reverse-listen
|
||||||
name: Wave 4 — the client -R leg (forward requests + the acceptor registry)
|
name: "Wave 4: the client -R leg (forward requests + the acceptor registry)"
|
||||||
status: completed
|
status: completed
|
||||||
depends_on: [w4-op-rows]
|
depends_on: [w4-op-rows]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
@@ -15,44 +15,45 @@ Land the client direction's `-R` leg (`client.md` §Op rows row 4 +
|
|||||||
§The acceptor): the hub asks the vanilla server to listen, then
|
§The acceptor): the hub asks the vanilla server to listen, then
|
||||||
**accepts the server-initiated `forwarded-tcpip` opens** and routes
|
**accepts the server-initiated `forwarded-tcpip` opens** and routes
|
||||||
each to the family handler. The acceptor row is unavoidable and its
|
each to the family handler. The acceptor row is unavoidable and its
|
||||||
disposition is ours (reverse-runner fact 1) — russh's client default
|
disposition is ours (reverse-runner fact 1), because russh's client default
|
||||||
is accept-and-drop, a half-capability the family forbids; the
|
is accept-and-drop, a half-capability the family forbids; the
|
||||||
`w4-dial` skeleton pins the fail-closed default, this task grows the
|
`w4-dial` skeleton pins the fail-closed default, this task grows the
|
||||||
registry that serves:
|
registry that serves:
|
||||||
|
|
||||||
- **The forward request rows** — `tcpip_forward(address, port) ->
|
- **The forward request rows**: `tcpip_forward(address, port) ->
|
||||||
bound` (russh's client `Handle` returns the actually-bound `u32`;
|
bound` (russh's client `Handle` returns the actually-bound `u32`;
|
||||||
`port = 0` → the server-picked number surfaced — the lifecycle
|
`port = 0` → the server-picked number surfaced, the lifecycle
|
||||||
primitive `client.md` §Lifecycle pins), `cancel_tcpip_forward`,
|
primitive `client.md` §Lifecycle pins), `cancel_tcpip_forward`,
|
||||||
`streamlocal_forward` + `cancel_streamlocal_forward` with the
|
`streamlocal_forward` + `cancel_streamlocal_forward` with the
|
||||||
**runtime-Unix gate** (OQ-11 symmetry: universal compile, clean
|
**runtime-Unix gate** (OQ-11 symmetry: universal compile, clean
|
||||||
off-Unix decline — the dial-call-witness posture the server side
|
off-Unix decline, the dial-call-witness posture the server side
|
||||||
landed).
|
landed).
|
||||||
- **The acceptor registry** — the assembly-mutable, per-channel-type
|
- **The acceptor registry**, the assembly-mutable, per-channel-type
|
||||||
registry the client Handler routes server-initiated opens through:
|
registry the client Handler routes server-initiated opens through:
|
||||||
served **only** under a registered acceptor **for that type and
|
served **only** under a registered acceptor **for that type and
|
||||||
under its feature** (`forwarded-tcpip` / `forwarded-streamlocal`
|
under its feature** (`forwarded-tcpip` / `forwarded-streamlocal`
|
||||||
under `tunnel` is the real case); unregistered/unfeatured accepted
|
under `tunnel` is the real case); unregistered/unfeatured accepted
|
||||||
opens **declined cleanly** — never silently dropped after accept
|
opens **declined cleanly**, never silently dropped after accept
|
||||||
(ADR-003 §4). Decide + record the registry's exact shape (per-type
|
(ADR-003 §4). Decide + record the registry's exact shape (per-type
|
||||||
slots vs a typed map; the acceptor trait signature — what an
|
slots vs a typed map; the acceptor trait signature, naming what an
|
||||||
accepted open hands the assembly: the channel duplex + the open's
|
accepted open hands the assembly: the channel duplex + the open's
|
||||||
originator fields) against the harness's `ForwardedAcceptor`
|
originator fields) against the harness's `ForwardedAcceptor`
|
||||||
vocabulary (`w3-tunnel-listen` landed the first shape client-side
|
vocabulary (`w3-tunnel-listen` landed the first shape client-side
|
||||||
in tests — grow it into the crate's real surface, don't fork it).
|
in tests; grow it into the crate's real surface, don't fork it).
|
||||||
- **Late-install across a live connection** (the alkminer
|
- **Late-install across a live connection** (the alkminer
|
||||||
`install_forward_sink` dynamic — reverse-runner fact 2): acceptors
|
`install_forward_sink` dynamic, reverse-runner fact 2): acceptors
|
||||||
install/uninstall while the connection is up; consumers turn
|
install/uninstall while the connection is up; consumers turn
|
||||||
capabilities on/off per connection. An open arriving before its
|
capabilities on/off per connection. An open arriving before its
|
||||||
acceptor installs is declined (fail-closed at arrival time —
|
acceptor installs is declined (fail-closed at arrival time, with a decide-and-record;
|
||||||
decide + record; no buffering of unclaimed opens).
|
no buffering of unclaimed opens).
|
||||||
- **The data leg** — a registered acceptor's open: the channel duplex
|
- **The data leg**, a registered acceptor's open: the channel duplex
|
||||||
handed to the assembly handler (the pump is the acceptor's, per the
|
handed to the assembly handler (the pump is the acceptor's, per the
|
||||||
family handler shape); bounded bookkeeping (no unbounded registry,
|
family handler shape); bounded bookkeeping (no unbounded registry,
|
||||||
no unbounded per-connection listener map — the `Limits` cap
|
no unbounded per-connection listener map, with the `Limits` cap
|
||||||
relation decided + recorded, the server-side listener-counter
|
relation decided + recorded, the server-side listener-counter
|
||||||
posture as the template).
|
posture as the template).
|
||||||
- **Contract rows through the server double, per build** — the fake
|
- **Contract rows through the server double, per build**, with the
|
||||||
|
fake
|
||||||
`sshd` grown with the server-initiated-open surfaces: featured +
|
`sshd` grown with the server-initiated-open surfaces: featured +
|
||||||
registered → served; featured + unregistered → clean decline;
|
registered → served; featured + unregistered → clean decline;
|
||||||
unfeatured → clean decline (per-build matrices, the symmetry rule);
|
unfeatured → clean decline (per-build matrices, the symmetry rule);
|
||||||
@@ -61,7 +62,7 @@ registry that serves:
|
|||||||
off-Unix streamlocal rows; the bounded-bookkeeping rows. The
|
off-Unix streamlocal rows; the bounded-bookkeeping rows. The
|
||||||
real-tool smoke posture decided + recorded (a real `ssh -R`-shaped
|
real-tool smoke posture decided + recorded (a real `ssh -R`-shaped
|
||||||
round trip against the double is the in-process form; real-`sshd`
|
round trip against the double is the in-process form; real-`sshd`
|
||||||
`-R` is wave-5 e2e — never silently dropped).
|
`-R` is wave-5 e2e, never silently dropped).
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ registry that serves:
|
|||||||
- [x] The acceptor registry lands (shape decided + recorded): served
|
- [x] The acceptor registry lands (shape decided + recorded): served
|
||||||
only under a registered acceptor under `tunnel`;
|
only under a registered acceptor under `tunnel`;
|
||||||
unregistered/unfeatured server-initiated opens declined cleanly
|
unregistered/unfeatured server-initiated opens declined cleanly
|
||||||
(never accept-and-drop) — wire rows per build
|
(never accept-and-drop), wire rows per build
|
||||||
- [x] Late-install works across a live connection; a pre-install
|
- [x] Late-install works across a live connection; a pre-install
|
||||||
arrival is declined (decided + recorded, no unclaimed buffering)
|
arrival is declined (decided + recorded, no unclaimed buffering)
|
||||||
- [x] The data leg hands accepted opens to the assembly handler;
|
- [x] The data leg hands accepted opens to the assembly handler;
|
||||||
@@ -87,16 +88,16 @@ registry that serves:
|
|||||||
- docs/architecture/client.md §Op rows (row 4), §The acceptor
|
- docs/architecture/client.md §Op rows (row 4), §The acceptor
|
||||||
(fail-closed client-side), §Lifecycle primitives
|
(fail-closed client-side), §Lifecycle primitives
|
||||||
- docs/architecture/decisions/003-feature-graph.md (§4 the
|
- docs/architecture/decisions/003-feature-graph.md (§4 the
|
||||||
client-side fail-closed acceptor — the rule this task implements),
|
client-side fail-closed acceptor, the rule this task implements),
|
||||||
docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
|
||||||
(route 1, both legs)
|
(route 1, both legs)
|
||||||
- docs/research/reverse-runner.md (facts 1–2 — the acceptor row's
|
- docs/research/reverse-runner.md (facts 1–2, the acceptor row's
|
||||||
disposition is ours; the late-install dynamic; lesson 1), docs/research/russh.md
|
disposition is ours; the late-install dynamic; lesson 1), docs/research/russh.md
|
||||||
§Client surface (the `server_channel_open_*` hook set; the
|
§Client surface (the `server_channel_open_*` hook set; the
|
||||||
`tcpip_forward -> u32` signature)
|
`tcpip_forward -> u32` signature)
|
||||||
- docs/research/poc-3-mapping-bakeoff.md §C.4 (the fabric recipe —
|
- docs/research/poc-3-mapping-bakeoff.md §C.4 (the fabric recipe,
|
||||||
the family-side half the accepted opens feed)
|
the family-side half the accepted opens feed)
|
||||||
- tests/common/harness.rs (`ForwardedAcceptor` — the vocabulary to
|
- tests/common/harness.rs (`ForwardedAcceptor`, the vocabulary to
|
||||||
grow, not fork), src/tunnel_map.rs (the server-side listener
|
grow, not fork), src/tunnel_map.rs (the server-side listener
|
||||||
bookkeeping posture as the template)
|
bookkeeping posture as the template)
|
||||||
|
|
||||||
@@ -107,36 +108,38 @@ registry that serves:
|
|||||||
Implementation decisions (also recorded in `src/client_accept.rs`'s
|
Implementation decisions (also recorded in `src/client_accept.rs`'s
|
||||||
module docs and `client.md` §The acceptor's landing block):
|
module docs and `client.md` §The acceptor's landing block):
|
||||||
|
|
||||||
- **Registry shape — per-type fixed slots, not a typed map**: the
|
- **Registry shape (per-type fixed slots, not a typed map)**: the
|
||||||
client-direction registry-servable set is closed (exactly the two
|
client-direction registry-servable set is closed (exactly the two
|
||||||
`-R` data-leg types under `tunnel`); one acceptor trait per type
|
`-R` data-leg types under `tunnel`); one acceptor trait per type
|
||||||
(`TcpipForwardedAcceptor` / `StreamlocalForwardedAcceptor`), the
|
(`TcpipForwardedAcceptor` / `StreamlocalForwardedAcceptor`), the
|
||||||
payload an accepted open hands the assembly: the channel duplex
|
payload an accepted open hands the assembly: the channel duplex
|
||||||
(family-typed — the `-L` rows' `TunnelChannel`, one type both legs
|
(family-typed, the `-L` rows' `TunnelChannel` that both legs
|
||||||
ride) + the open's echo/originator fields — the harness
|
ride) plus the open's echo/originator fields, the harness
|
||||||
`ForwardedAcceptor` vocabulary grown, not forked. Install is
|
`ForwardedAcceptor` vocabulary grown and not forked. Install is
|
||||||
**refuse-never-replace** (`AcceptorHeld` — the server-side
|
**refuse-never-replace** (`AcceptorHeld`, the server-side
|
||||||
`ListenRouter::register` posture mirrored); uninstall is explicit
|
`ListenRouter::register` posture mirrored); uninstall is explicit
|
||||||
and reports honestly. A new module `src/client_accept.rs` (one
|
and reports honestly. A new module `src/client_accept.rs` (one
|
||||||
module per file; the rows' method bodies live beside the registry).
|
module per file; the rows' method bodies live beside the registry).
|
||||||
- **The listen rows gate on the registered acceptor** (the
|
- **The listen rows gate on the registered acceptor** (the
|
||||||
issuing-side disposition — a listen whose data leg cannot serve is
|
issuing-side disposition, so a listen whose data leg cannot serve
|
||||||
|
is
|
||||||
never asked for; install-first recorded order;
|
never asked for; install-first recorded order;
|
||||||
`OpError::NoAcceptor` pre-wire). The cancel forms do *not* gate on
|
`OpError::NoAcceptor` pre-wire). The cancel forms do *not* gate on
|
||||||
the acceptor (teardown must work after an uninstall).
|
the acceptor (teardown must work after an uninstall).
|
||||||
- **Bound-port mapping** (recorded wire fact at this pin): russh's
|
- **Bound-port mapping** (recorded wire fact at this pin): russh's
|
||||||
client `tcpip_forward` reply carries a port **only for
|
client `tcpip_forward` reply carries a port **only for
|
||||||
`port = 0` requests** (sshd parity — an explicit request's bound
|
`port = 0` requests** (sshd parity, since an explicit request's bound
|
||||||
port *is* the requested port); `reverse_listen` maps both to the
|
port *is* the requested port); `reverse_listen` maps both to the
|
||||||
actually-bound number it returns.
|
actually-bound number it returns.
|
||||||
- **Bounded bookkeeping — the cap relation**: two dedicated
|
- **Bounded bookkeeping (the cap relation)**: two dedicated
|
||||||
`ChannelCap` counters, each against the same
|
`ChannelCap` counters, each against the same
|
||||||
`Limits::max_concurrent_channels` ceiling (the server-side
|
`Limits::max_concurrent_channels` ceiling (the server-side
|
||||||
listener/ListenerCap posture mirrored): (a) the per-connection
|
listener/ListenerCap posture mirrored): (a) the per-connection
|
||||||
**reverse-listen count** — admitted at the listen rows, released at
|
**reverse-listen count**, admitted at the listen rows, released
|
||||||
confirmed cancels and the refused/wire-error paths (never a map —
|
at
|
||||||
the outstanding listens live at the peer); (b) the **accepted-open
|
confirmed cancels and the refused/wire-error paths (never a map,
|
||||||
handoff count** — admitted in the dial handler's open hook before
|
since the outstanding listens live at the peer); (b) the **accepted-open
|
||||||
|
handoff count**, admitted in the dial handler's open hook before
|
||||||
the accept, released when the acceptor's `accept` call completes in
|
the accept, released when the acceptor's `accept` call completes in
|
||||||
the spawned handoff task; a full counter declines the arrival
|
the spawned handoff task; a full counter declines the arrival
|
||||||
(`cap-exceeded`, event vocabulary). No accepted-open ledger: the
|
(`cap-exceeded`, event vocabulary). No accepted-open ledger: the
|
||||||
@@ -146,14 +149,15 @@ module docs and `client.md` §The acceptor's landing block):
|
|||||||
bind is the platform-bound op; `forwarded-streamlocal` *ingestion*
|
bind is the platform-bound op; `forwarded-streamlocal` *ingestion*
|
||||||
is platform-neutral byte pumping, hook universal compile).
|
is platform-neutral byte pumping, hook universal compile).
|
||||||
- **Pre-existing wart fixed**: `client_ops.rs`'s test mod gated
|
- **Pre-existing wart fixed**: `client_ops.rs`'s test mod gated
|
||||||
`family_exit_code` under a `tunnel`-inclusive cfg — the
|
`family_exit_code` under a `tunnel`-inclusive cfg, so the
|
||||||
`--features tunnel` build did not compile its tests before this
|
`--features tunnel` build did not compile its tests before this
|
||||||
task; the import/test are now gated to the exit-consuming builds.
|
task; the import/test are now gated to the exit-consuming builds.
|
||||||
|
|
||||||
Surprises: russh's client reply shapes (above, the port-0-only mint)
|
Surprises: russh's client reply shapes (above, the port-0-only mint)
|
||||||
and the `JoinHandle::drop`-is-detach-not-cancel behavior in the
|
and the `JoinHandle::drop`-is-detach-not-cancel behavior in the
|
||||||
double's cancel teardown (first draft removed the entry without
|
double's cancel teardown (first draft removed the entry without
|
||||||
aborting the accept loop — the real listener stayed bound; the double
|
aborting the accept loop, so the real listener stayed bound; the
|
||||||
|
double
|
||||||
aborts explicitly now).
|
aborts explicitly now).
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
@@ -173,49 +177,49 @@ Details in the `Landed` block below.
|
|||||||
|
|
||||||
Landed (`feat(client)`: w4-reverse-listen):
|
Landed (`feat(client)`: w4-reverse-listen):
|
||||||
|
|
||||||
- **`src/client_accept.rs`** — the acceptor registry
|
- **`src/client_accept.rs`**: the acceptor registry
|
||||||
(`AcceptorRegistry` + the two per-type acceptor traits + the
|
(`AcceptorRegistry` + the two per-type acceptor traits + the
|
||||||
`ForwardedTcpip`/`ForwardedStreamlocal` payloads + `AcceptorHeld`),
|
`ForwardedTcpip`/`ForwardedStreamlocal` payloads + `AcceptorHeld`),
|
||||||
the reverse-listen rows (`reverse_listen` / `reverse_listen_cancel`
|
the reverse-listen rows (`reverse_listen` / `reverse_listen_cancel`
|
||||||
/ the unix pair, with the gates + dedicated counters), and the
|
/ the unix pair, with the gates + dedicated counters), and the
|
||||||
accepted-opens' handoff tasks; exports from the crate root under
|
accepted-opens' handoff tasks; exports from the crate root under
|
||||||
`tunnel`.
|
`tunnel`.
|
||||||
- **`src/client.rs`** — the dial wires the (empty) registry +
|
- **`src/client.rs`**: the dial wires the (empty) registry plus
|
||||||
tunnel surface into the handler and the connection
|
tunnel surface into the handler and the connection
|
||||||
(`SshConnection::acceptors` — the live-connection accessor;
|
(`SshConnection::acceptors`, the live-connection accessor;
|
||||||
the listener budget's private field).
|
the listener budget's private field).
|
||||||
- **`src/client_handler.rs`** — the two forwarded hooks route through
|
- **`src/client_handler.rs`**, the two forwarded hooks route through
|
||||||
the registry at arrival time (registered + room → accept + spawned
|
the registry at arrival time (registered + room → accept + spawned
|
||||||
handoff; unregistered/cap-exhausted → clean `Administratively-
|
handoff; unregistered/cap-exhausted → clean `Administratively-
|
||||||
Prohibited` decline with the event vocabulary); every other type
|
Prohibited` decline with the event vocabulary); every other type
|
||||||
keeps the decline-all skeleton; the never-feature rows unchanged.
|
keeps the decline-all skeleton; the never-feature rows unchanged.
|
||||||
- **`src/error.rs`** — `OpError::NoAcceptor` / `ListenRefused` /
|
- **`src/error.rs`**, holding `OpError::NoAcceptor` / `ListenRefused` /
|
||||||
`ListenCapExceeded` + the `RequestDenied → ListenRefused` mapping
|
`ListenCapExceeded` + the `RequestDenied → ListenRefused` mapping
|
||||||
(the listen/cancel forms' only wire decline shape at this pin).
|
(the listen/cancel forms' only wire decline shape at this pin).
|
||||||
- **`tests/common/sshd_double.rs`** — the fake `sshd`'s `-R` server
|
- **`tests/common/sshd_double.rs`**, the fake `sshd`'s `-R` server
|
||||||
half: the four listen/cancel hooks (scripted accept/deny via
|
half: the four listen/cancel hooks (scripted accept/deny via
|
||||||
`DblListen`; an accept binds a **real** socket, mints the real
|
`DblListen`; an accept binds a **real** socket, mints the real
|
||||||
bound port for `port = 0` into the out-param, spawns the accept
|
bound port for `port = 0` into the out-param, spawns the accept
|
||||||
loop — per accepted connection a server-initiated `forwarded-*`
|
loop; per accepted connection a server-initiated `forwarded-*`
|
||||||
open echoing the listen's args + the peer, then the bytes pumped;
|
open echoing the listen's args + the peer, then the bytes pumped;
|
||||||
cancels match by the raw wire strings, tearing the real listener
|
cancels match by the raw wire strings, tearing the real listener
|
||||||
down) + the listen/cancel observations.
|
down) + the listen/cancel observations.
|
||||||
- **`tests/client_reverse_listen.rs`** (`tunnel` build) — the
|
- **`tests/client_reverse_listen.rs`** (`tunnel` build), carrying the
|
||||||
contract rows: listen + data-leg pump round trip (real
|
contract rows: listen + data-leg pump round trip (real
|
||||||
`ssh -R`-shaped), the port-0 mint, the no-acceptor pre-wire
|
`ssh -R`-shaped), the port-0 mint, the no-acceptor pre-wire
|
||||||
declines (listen + open, with the event rows), the late-install
|
declines (listen + open, with the event rows), the late-install
|
||||||
rows (pre-install declined → serve-after; uninstall takes it off),
|
rows (pre-install declined → serve-after; uninstall takes it off),
|
||||||
the cancel rows (teardown observable — reconnect refused; unknown
|
the cancel rows (teardown observable: reconnect refused; unknown
|
||||||
→ `ListenRefused`), the listener-ceiling row (exceeded pre-wire;
|
→ `ListenRefused`), the listener-ceiling row (exceeded pre-wire;
|
||||||
cancel recovers), the registry contract through the accessor, and
|
cancel recovers), the registry contract through the accessor, and
|
||||||
the streamlocal unix rows (+ the compiled off-Unix gate row).
|
the streamlocal unix rows (+ the compiled off-Unix gate row).
|
||||||
Per-build matrix: the *unfeatured* builds' wire rows run in every
|
Per-build matrix: the *unfeatured* builds' wire rows run in every
|
||||||
build through `tests/client_dial.rs`'s decline-all row (the
|
build through `tests/client_dial.rs`'s decline-all row (the
|
||||||
surface itself is the compile-time mirror).
|
surface itself is the compile-time mirror).
|
||||||
- **`tests/tunnel_mapping.rs`** — the `client_smoke` module: the
|
- **`tests/tunnel_mapping.rs`**, the `client_smoke` module: the
|
||||||
full-circle alkssh-client ↔ alkssh-server-direction smoke (both
|
full-circle alkssh-client ↔ alkssh-server-direction smoke (both
|
||||||
`-R` legs through the alkcall fabric double over a real TCP
|
`-R` legs through the alkcall fabric double over a real TCP
|
||||||
mount) — the port-0 mint through the server direction's fabric
|
mount), with the port-0 mint through the server direction's fabric
|
||||||
listen + arrival pump into the registered acceptor's duplex, bytes
|
listen + arrival pump into the registered acceptor's duplex, bytes
|
||||||
round-tripped end to end; real-`sshd` `-R` remains wave-5 e2e
|
round-tripped end to end; real-`sshd` `-R` remains wave-5 e2e
|
||||||
(recorded, never silently dropped).
|
(recorded, never silently dropped).
|
||||||
|
|||||||
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user