Laborator 6: construiește un spațiu de lucru reproductibil în linia de comandă

Laborator practic Ubuntu/Kubuntu pentru Lecția 6: construiește un spațiu de lucru reproductibil în linia de comandă, verifică PATH și mediul proceselor, demonstrează pipefail, rulează un serviciu systemd al utilizatorului, citește jurnalul și inspectează configurația SSH.

Laboratorul 0654 min de lectură

180–240 de minute · Ubuntu/Kubuntu · Nivel: începător · Continuă direct proiectul din Laboratoarele 1–5

Ce construim și ce trebuie să înțelegi

În teoria Lecției 6 ai văzut că linia de comandă nu este o colecție de incantații, ci un mod de a porni procese într-un context: folder curent, PATH, variabile de mediu, descriptori standard, coduri de ieșire și, uneori, un manager de servicii sau o conexiune SSH. În acest laborator transformăm acel model într-un spațiu de lucru pe care îl poți verifica și recrea.

Nu pornim un proiect nou. Lucrăm în același ai-agents-zero-to-expert construit în Laboratoarele 1–5. Păstrăm aceeași disciplină: sursele și deciziile intră în Git; rezultatele specifice calculatorului și sesiunii rămân locale; testele trebuie să dovedească regulile înainte de commit; diagrama și ADR-ul devin documentație versionată.

Dovada practică a Lecției 6

La final trebuie să poți lua proiectul dintr-un shell nou și să explici, fără presupuneri ascunse: unde este rădăcina proiectului, ce comenzi sunt necesare, ce executabile vor fi alese prin PATH, ce variabile trebuie exportate, unde ajung stdout și stderr, cum este detectat un eșec, cum pornește același proces de test ca serviciu systemd și unde îi citești logurile.

Regulă de siguranță: sudo apare numai la instalarea pachetelor de sistem. Serviciul didactic este un serviciu al utilizatorului și se rulează cu systemctl --user, nu ca root. Laboratorul nu instalează și nu reconfigurează automat un server SSH.

1. Vezi de la început rezultatul final

Ca în laboratoarele anterioare, începem cu harta artefactelor. Fiecare fișier are o responsabilitate distinctă.

ai-agents-zero-to-expert/
├── configs/
│   ├── lesson06_workspace_manifest.json
│   ├── lesson06.env.example
│   └── systemd/
│       └── lesson06-workspace.service.in
├── data/
│   └── lesson06/
│       ├── workspace_snapshot.json      # generat local, ignorat de Git
│       └── runtime/                     # generat local, ignorat
├── diagrams/
│   ├── lesson06_workspace_flow.dot
│   └── lesson06_workspace_flow.png
├── docs/
│   ├── architecture-decisions/
│   │   └── ADR-0006-explicit-command-line-context.md
│   └── lessons/
│       └── lesson-06/
│           ├── workspace-contract.md
│           └── workspace_report.md      # generat local, ignorat
├── logs/
│   └── lesson06/
│       └── .gitkeep
├── scripts/
│   ├── lesson06_workspace.sh
│   └── lesson06_worker.sh
└── tests/
    └── test_lesson06_workspace.sh

Observă schimbarea față de Lecțiile 2–5: instrumentul principal al acestui laborator este Bash, nu Python. Nu abandonăm codul Python existent; pur și simplu nu îl extindem încă. Lecția 7 va preda Python ca limbaj. Aici ne interesează mediul din care acel Python va fi pornit.

2. Stabilește limitele laboratorului

Facem

  • declarăm comenzile obligatorii și contextuale;
  • demonstrăm PATH și variabile exportate;
  • separăm stdout de stderr;
  • demonstrăm efectul pipefail;
  • generăm o captură de stare limitată a contextului;
  • creăm un proces Bash de test inspectabil;
  • rulăm procesul de test ca serviciu systemd al utilizatorului, dacă sesiunea permite;
  • citim jurnalul serviciului;
  • inspectăm configurația clientului SSH fără a ne conecta la un server;
  • scriem teste și ADR.

Nu facem

  • nu administrăm încă un server de producție;
  • nu instalăm sau configurăm automat sshd;
  • nu publicăm întregul mediu al procesului;
  • nu punem chei API în exemple;
  • nu folosim curl | bash;
  • nu transformăm procesul de test într-un daemon real de producție;
  • nu predăm încă programare Python;
  • nu introducem containere — acestea rămân pentru Lecția 10.

3. Leagă Laboratorul 6 de ceea ce ai construit deja

Laboratorul 5 a încheiat cu întrebarea: ce proces execută acțiunea, cu ce identitate și ce permisiuni? Laboratorul 6 adaugă întrebarea imediat următoare: în ce context este pornit acel proces?

Laboratorul 2stare explicită, acțiuni delimitate, coduri care pot opri fluxul;
Laboratorul 3inventar observat separat de arhitectura decisă;
Laboratorul 4responsabilitatea înaintea numelui produsului;
Laboratorul 5identitatea și autoritatea procesului;
Laboratorul 6CWD, PATH, mediu, I/O, cod de ieșire, manager de servicii și acces la distanță declarate explicit.

Principiul comun rămâne același: nu confunda ceea ce observi pe calculatorul tău cu ceea ce proiectul cere. Captura de stare spune „ce există acum”; manifestul spune „ce contract vrem să respecte spațiul de lucru”.

4. Instrumentele folosite

Bash

Shell-ul și limbajul scripturilor acestui laborator.

jq

Validează manifestul JSON și construiește captura de stare fără concatenare manuală de JSON.

Git

Separă sursa revizuită de rezultatele locale.

Graphviz

Generează harta contextului de execuție.

systemd

Arată diferența dintre un proces pornit manual și un serviciu administrat.

journalctl

Arată unde ajung stdout/stderr când procesul de test rulează ca serviciu.

OpenSSH client

Inspectează configurația clientului și pregătește lucrul la distanță fără a instala server SSH.

5. Continuă proiectul cumulativ

cd ~/ai-agents-zero-to-expert
pwd

În traseul standard al cursului, calea afișată se termină în /ai-agents-zero-to-expert. Nu este important ce apare înainte; folderul personal diferă de la utilizator la utilizator.

Creează numai folderele noi:

mkdir -p \
  configs/systemd \
  data/lesson06 \
  diagrams \
  docs/architecture-decisions \
  docs/lessons/lesson-06 \
  logs/lesson06 \
  scripts \
  tests

touch logs/lesson06/.gitkeep

6. Verifică și instalează instrumentele de sistem

Mai întâi vezi ce există deja:

for cmd in bash git jq dot tree systemctl journalctl ssh ssh-keygen; do
  printf '%-12s ' "$cmd"
  command -v "$cmd" || true
done

Pe Ubuntu/Kubuntu, instalează utilitarele de laborator care lipsesc:

sudo apt update
sudo apt install -y jq tree graphviz openssh-client

Nu instalăm openssh-server doar pentru această lecție. Clientul este suficient pentru secțiunea obligatorie SSH.

Folosește APT și în mod de inspecție:

apt-cache policy jq
dpkg-query -W -f='${db:Status-Status} ${binary:Package} ${Version}\n' jq
apt-get -s install jq

apt-get -s simulează operația; nu schimbă sistemul. Este un exemplu bun de „inspectează înainte să modifici”.

7. Activează mediul virtual și observă ce se schimbă

Mediul virtual există deja din laboratoarele anterioare:

source .venv/bin/activate

printf 'VIRTUAL_ENV=%s\n' "$VIRTUAL_ENV"
command -v python
printf '%s\n' "$PATH" | tr ':' '\n' | head

Nu instalăm nimic în Python în această lecție. Observăm doar efectul: scriptul de activare modifică shell-ul curent, iar .venv/bin ajunge înaintea altor locații în PATH. Asta pregătește întrebarea esențială din Lecția 7: ce Python rulează de fapt?

8. Extinde .gitignore fără să ascunzi sursele importante

touch .gitignore

if ! grep -q '^# Laboratorul 6:' .gitignore; then
  cat >> .gitignore <<'EOF'

# Laboratorul 6: rezultate specifice sesiunii și calculatorului
/data/lesson06/runtime/
/data/lesson06/workspace_snapshot.json
/docs/lessons/lesson-06/workspace_report.md
!/logs/lesson06/
/logs/lesson06/*
!/logs/lesson06/.gitkeep
EOF
fi

touch logs/lesson06/.gitkeep

De ce apar trei reguli pentru logs/lesson06? Laboratoarele anterioare pot avea deja regula generală logs/*. Mai întâi repermitem folderul logs/lesson06/, apoi ignorăm conținutul lui și repermitem numai .gitkeep. Altfel, Git nu poate reinclude un fișier aflat într-un folder deja ignorat.

Manifestul, scripturile, testele, șablonul systemd, ADR-ul și diagrama nu sunt ignorate. Ele descriu contractul. Captura de stare și raportul observat depind de mașină și sesiune.

9. Creează manifestul spațiului de lucru

Creează fișierul complet cu un here-document. Marcajul JSON delimitează conținutul; nu face parte din fișier.

cat > configs/lesson06_workspace_manifest.json <<'JSON'
{
  "schema_version": 1,
  "lesson": 6,
  "purpose": "Contract reproductibil pentru mediul de linie de comandă al proiectului cumulativ.",
  "shell": {
    "name": "bash",
    "minimum_major": 5
  },
  "commands": [
    {
      "name": "bash",
      "class": "required",
      "purpose": "shell-ul folosit de laborator"
    },
    {
      "name": "git",
      "class": "required",
      "purpose": "inspecție și versionare a proiectului cumulativ"
    },
    {
      "name": "jq",
      "class": "required",
      "purpose": "validare JSON și rapoarte structurate"
    },
    {
      "name": "dot",
      "class": "required",
      "purpose": "generarea diagramei Graphviz"
    },
    {
      "name": "tree",
      "class": "optional",
      "purpose": "afișare comodă a structurii proiectului"
    },
    {
      "name": "systemctl",
      "class": "contextual",
      "purpose": "administrarea serviciului systemd de utilizator"
    },
    {
      "name": "journalctl",
      "class": "contextual",
      "purpose": "citirea jurnalului serviciului"
    },
    {
      "name": "ssh",
      "class": "contextual",
      "purpose": "inspecția configurației clientului SSH și acces la distanță ulterior"
    },
    {
      "name": "ssh-keygen",
      "class": "contextual",
      "purpose": "inspecția intrărilor known_hosts"
    }
  ],
  "environment": [
    {
      "name": "LESSON06_MODE",
      "required": true,
      "secret": false,
      "example": "training"
    },
    {
      "name": "LESSON06_MESSAGE",
      "required": false,
      "secret": false,
      "example": "workspace-ready"
    }
  ],
  "runtime_outputs": [
    "data/lesson06/workspace_snapshot.json",
    "data/lesson06/runtime/",
    "docs/lessons/lesson-06/workspace_report.md",
    "logs/lesson06/"
  ],
  "versioned_artifacts": [
    "configs/lesson06_workspace_manifest.json",
    "configs/lesson06.env.example",
    "configs/systemd/lesson06-workspace.service.in",
    "scripts/lesson06_workspace.sh",
    "scripts/lesson06_worker.sh",
    "tests/test_lesson06_workspace.sh",
    "docs/lessons/lesson-06/workspace-contract.md",
    "docs/architecture-decisions/ADR-0006-explicit-command-line-context.md",
    "diagrams/lesson06_workspace_flow.dot",
    "diagrams/lesson06_workspace_flow.png"
  ]
}
JSON

Verifică-l:

jq empty configs/lesson06_workspace_manifest.json
jq -r '.commands[] | [.name, .class] | @tsv' \
  configs/lesson06_workspace_manifest.json

Observă cele trei clase:

  • required — lipsa lor face spațiul de lucru de bază incomplet;
  • optional — îmbunătățesc experiența, dar nu schimbă contractul;
  • contextual — au sens numai când mediul oferă posibilitatea, de exemplu o sesiune systemd a utilizatorului sau un client SSH.

10. Creează o configurație de mediu fără secrete

cat > configs/lesson06.env.example <<'ENV'
# Configurație didactică — NU conține secrete.
LESSON06_MODE=training
LESSON06_MESSAGE=workspace-ready
ENV

Inspectează fișierul înainte să îl încarci:

cat configs/lesson06.env.example

Apoi exportă valorile din acest fișier didactic:

set -a
source configs/lesson06.env.example
set +a

printf 'LESSON06_MODE=%s\n' "$LESSON06_MODE"
printf 'LESSON06_MESSAGE=%s\n' "$LESSON06_MESSAGE"

set -a face ca variabilele definite sau încărcate după activare să fie exportate automat către procesele-copil. set +a oprește acest comportament și readuce shell-ul la modul obișnuit. Folosim perechea numai în jurul fișierului didactic pe care tocmai l-ai inspectat.

source execută cod Bash în shell-ul curent. Îl folosim aici numai pentru un fișier pe care tocmai l-ai creat și inspectat. Nu face source pe fișiere necunoscute doar pentru că se numesc .env.

11. Creează instrumentul principal al spațiului de lucru

Creează scriptul complet prin here-document. Este mai sigur decât să copiezi bucăți separate: la final, verificarea bash -n trebuie să treacă înainte să continui.

Harta funcțiilor

  1. require_manifest() verifică existența manifestului, prezența lui jq și forma de bază a fișierului (schema_version, lesson, commands, environment) și întoarce 2 dacă ceva lipsește. Este separată pentru că orice subcomandă care citește manifestul trebuie să eșueze din aceeași cauză, cu același mesaj.
  2. command_path() primește numele unei comenzi și întoarce calea rezolvată prin PATH sau nimic. Este separată pentru că „unde este comanda" se întreabă în multe locuri, iar răspunsul trebuie să vină dintr-un singur loc: command -v.
  3. doctor() compară manifestul cu mașina: comenzile required, optional și contextual, variabilele de mediu obligatorii, apoi versiunea majoră Bash; întoarce 0 numai dacă nimic required nu lipsește. Este separată pentru că verdictul „spațiul de lucru este complet" este contractul laboratorului.
  4. context() afișează un context limitat: rădăcina proiectului, CWD, UID/GID, versiunea și calea Bash, cele două variabile ale lecției și intrările PATH. Este separată pentru că enumeră numai ce declară manifestul, nu întregul mediu — protecția față de secrete stă aici.
  5. env_demo() definește o variabilă locală și una exportată, apoi pornește un bash -c care arată ce ajunge la copil. Este separată pentru că demonstrația moștenirii mediului trebuie să fie reproductibilă dintr-o singură comandă.
  6. path_demo() creează două executabile cu același nume în două foldere locale ignorate de Git și le rulează cu PATH în ambele ordini. Este separată pentru că modifică PATH numai într-un subshell, fără să atingă sesiunea ta.
  7. producer_failure() și pipeline_demo() — prima scrie date pe stdout, un diagnostic pe stderr și iese cu 7; a doua o trece prin cat fără și cu pipefail și raportează ambele coduri. Sunt separate pentru că opțiunea pipefail este salvată și restaurată în jurul demonstrației.
  8. systemd_preflight() și ssh_preflight() verifică dacă există un manager systemd al utilizatorului, respectiv un client SSH cu configurație evaluabilă (ssh -G), fără să pornească servicii sau conexiuni; întorc 3 sau 4 când componenta lipsește. Sunt separate pentru că sunt verificări contextuale: absența lor nu este un eșec al spațiului de lucru.
  9. report() scrie captura de stare JSON și raportul Markdown din aceleași date pe care le afișează doctor și context. Este separată pentru că fișierele generate sunt locale și ignorate de Git — raportul descrie ce s-a observat, manifestul ce se cere.
  10. dot_graph() scrie sursa DOT a diagramei contextului de execuție, iar usage() și main() afișează ajutorul și trimit fiecare subcomandă către funcția ei. Sunt separate pentru că diagrama este generată, nu desenată de mână, iar main este singurul loc unde se interpretează argumentele.
cat > scripts/lesson06_workspace.sh <<'WORKSPACE_SH'
#!/usr/bin/env bash
set -u

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
PROJECT_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd -P)"
MANIFEST="$PROJECT_ROOT/configs/lesson06_workspace_manifest.json"
RUNTIME_DIR="$PROJECT_ROOT/data/lesson06/runtime"
SNAPSHOT="$PROJECT_ROOT/data/lesson06/workspace_snapshot.json"
REPORT="$PROJECT_ROOT/docs/lessons/lesson-06/workspace_report.md"

say() { printf '%s\n' "$*"; }
err() { printf 'EROARE: %s\n' "$*" >&2; }

require_manifest() {
  [[ -f "$MANIFEST" ]] || { err "Lipsește $MANIFEST"; return 2; }
  command -v jq >/dev/null 2>&1 || { err "jq este necesar pentru acest laborator."; return 2; }
  jq -e '.schema_version == 1 and .lesson == 6 and (.commands | type == "array") and (.environment | type == "array")' "$MANIFEST" >/dev/null \
    || { err "Manifest invalid: verifică schema_version, lesson, commands și environment."; return 2; }
}

command_path() {
  command -v -- "$1" 2>/dev/null || true
}

doctor() {
  require_manifest || return $?
  local missing=0 name class path required
  printf '%-14s %-11s %-10s %s
' 'COMANDĂ' 'CLASĂ' 'STARE' 'CALE'
  printf '%-14s %-11s %-10s %s
' '--------------' '-----------' '----------' '------------------------------'
  while IFS=$'	' read -r name class; do
    path="$(command_path "$name")"
    if [[ -n "$path" ]]; then
      printf '%-14s %-11s %-10s %s
' "$name" "$class" 'prezent' "$path"
    else
      printf '%-14s %-11s %-10s %s
' "$name" "$class" 'absent' '-'
      [[ "$class" == 'required' ]] && missing=1
    fi
  done < <(jq -r '.commands[] | [.name,.class] | @tsv' "$MANIFEST")

  printf '
VARIABILE DE MEDIU OBLIGATORII
'
  while IFS=$'	' read -r name required; do
    [[ "$required" == 'true' ]] || continue
    if [[ -v "$name" ]]; then
      printf '  %-22s %s
' "$name" 'prezentă'
    else
      printf '  %-22s %s
' "$name" 'LIPSĂ'
      missing=1
    fi
  done < <(jq -r '.environment[] | [.name, (.required|tostring)] | @tsv' "$MANIFEST")

  printf '
Versiune majoră Bash: %s
' "${BASH_VERSINFO[0]}"
  printf 'Rădăcină proiect: %s
' "$PROJECT_ROOT"
  if (( BASH_VERSINFO[0] < 5 )); then
    err 'Laboratorul presupune Bash 5 sau mai nou.'
    missing=1
  fi
  return "$missing"
}

context() {
  require_manifest || return $?
  printf 'project_root=%s\n' "$PROJECT_ROOT"
  printf 'cwd=%s\n' "$PWD"
  printf 'uid=%s\n' "$(id -u)"
  printf 'gid=%s\n' "$(id -g)"
  printf 'bash_version=%s\n' "$BASH_VERSION"
  printf 'bash_path=%s\n' "$(command_path bash)"
  printf 'LESSON06_MODE=%s\n' "${LESSON06_MODE-<unset>}"
  printf 'LESSON06_MESSAGE=%s\n' "${LESSON06_MESSAGE-<unset>}"
  printf 'Intrări PATH:\n'
  local item
  while IFS= read -r item; do printf '  - %s\n' "$item"; done < <(printf '%s' "$PATH" | tr ':' '\n')
  printf 'privacy_note=Ieșirea nu enumeră întregul mediu al procesului. CWD și PATH pot conține căi locale; nu publica rezultatul fără verificare.\n'
}

env_demo() {
  local LOCAL_ONLY='doar-shell'
  export LESSON06_EXPORTED='ajunge-la-copil'
  printf 'parent_LOCAL_ONLY=%s\n' "$LOCAL_ONLY"
  printf 'parent_LESSON06_EXPORTED=%s\n' "$LESSON06_EXPORTED"
  bash -c 'printf "child_LOCAL_ONLY=%s\\n" "${LOCAL_ONLY-<unset>}"; printf "child_LESSON06_EXPORTED=%s\\n" "${LESSON06_EXPORTED-<unset>}"'
}

path_demo() {
  local a="$RUNTIME_DIR/path-a" b="$RUNTIME_DIR/path-b"
  mkdir -p "$a" "$b"
  cat > "$a/lesson06-hello" <<'EOF'
#!/usr/bin/env bash
printf 'source=path-a\n'
EOF
  cat > "$b/lesson06-hello" <<'EOF'
#!/usr/bin/env bash
printf 'source=path-b\n'
EOF
  chmod 0755 "$a/lesson06-hello" "$b/lesson06-hello"
  (
    PATH="$a:$b:$PATH"
    printf 'first_path=%s\n' "$(command -v lesson06-hello)"
    lesson06-hello
    PATH="$b:$a:$PATH"
    printf 'second_path=%s\n' "$(command -v lesson06-hello)"
    lesson06-hello
  )
}

producer_failure() {
  printf 'payload=valid-data\n'
  printf 'diagnostic=producer-failed-after-output\n' >&2
  return 7
}

pipeline_demo() {
  mkdir -p "$RUNTIME_DIR"
  local out1="$RUNTIME_DIR/pipeline-no-pipefail.out"
  local err1="$RUNTIME_DIR/pipeline-no-pipefail.err"
  local out2="$RUNTIME_DIR/pipeline-pipefail.out"
  local err2="$RUNTIME_DIR/pipeline-pipefail.err"
  local without with pipefail_before
  pipefail_before="$(set -o | awk '$1=="pipefail" {print $2}')"

  set +o pipefail
  producer_failure 2>"$err1" | cat >"$out1"
  without=$?

  set -o pipefail
  producer_failure 2>"$err2" | cat >"$out2"
  with=$?

  [[ "$pipefail_before" == 'off' ]] && set +o pipefail
  printf 'status_without_pipefail=%s\n' "$without"
  printf 'status_with_pipefail=%s\n' "$with"
  printf 'stdout_file=%s\n' "$out2"
  printf 'stderr_file=%s\n' "$err2"
  printf 'stdout_content=%s\n' "$(tr '\n' ';' < "$out2")"
  printf 'stderr_content=%s\n' "$(tr '\n' ';' < "$err2")"
}

systemd_preflight() {
  command -v systemctl >/dev/null 2>&1 || { say 'systemd_user_manager=unavailable (systemctl absent)'; return 3; }
  if systemctl --user show-environment >/dev/null 2>&1; then
    say 'systemd_user_manager=available'
    return 0
  fi
  say 'systemd_user_manager=unavailable (no user manager/session bus)'
  return 3
}

ssh_preflight() {
  command -v ssh >/dev/null 2>&1 || { say 'ssh_client=unavailable'; return 3; }
  local cfg
  if ! cfg="$(ssh -G localhost 2>/dev/null)"; then
    say 'ssh_client=available (configurația nu poate fi evaluată)'
    return 4
  fi
  say 'ssh_client=available'
  awk 'BEGIN{IGNORECASE=1} $1=="hostname" || $1=="user" || $1=="port" {print "ssh_" $1 "=" $2}' <<<"$cfg" | head -n 3
}

report() {
  require_manifest || return $?
  mkdir -p "$(dirname "$SNAPSHOT")" "$(dirname "$REPORT")"
  local systemd_state='unavailable' ssh_state='unavailable'
  systemd_preflight >/dev/null 2>&1 && systemd_state='available'
  ssh_preflight >/dev/null 2>&1 && ssh_state='available'
  local required_json contextual_json
  required_json="$(jq -c '[.commands[] | select(.class=="required") | .name]' "$MANIFEST")"
  contextual_json="$(jq -c '[.commands[] | select(.class=="contextual") | .name]' "$MANIFEST")"
  jq -n \
    --arg project_root "$PROJECT_ROOT" \
    --arg cwd "$PWD" \
    --arg uid "$(id -u)" \
    --arg gid "$(id -g)" \
    --arg bash_version "$BASH_VERSION" \
    --arg lesson06_mode "${LESSON06_MODE-<unset>}" \
    --arg lesson06_message "${LESSON06_MESSAGE-<unset>}" \
    --arg systemd_user_manager "$systemd_state" \
    --arg ssh_client "$ssh_state" \
    --argjson required_commands "$required_json" \
    --argjson contextual_commands "$contextual_json" \
    '{schema_version:1, project_root:$project_root, cwd:$cwd, uid:$uid, gid:$gid, bash_version:$bash_version, selected_environment:{LESSON06_MODE:$lesson06_mode,LESSON06_MESSAGE:$lesson06_message}, contextual_availability:{systemd_user_manager:$systemd_user_manager,ssh_client:$ssh_client}, required_commands:$required_commands, contextual_commands:$contextual_commands, privacy_note:"Nu enumeră hostname-ul, întregul PATH sau mediul complet. Căile project_root și cwd pot conține informații locale, inclusiv numele contului; fișierul rămâne local și ignorat de Git."}' \
    > "$SNAPSHOT"

  {
    printf '# Raport al spațiului de lucru — Lecția 6\n\n'
    printf '## Context declarat\n\n'
    printf -- '- Rădăcină proiect: `%s`\n' "$PROJECT_ROOT"
    printf -- '- CWD la generarea raportului: `%s`\n' "$PWD"
    printf -- '- Bash: `%s`\n' "$BASH_VERSION"
    printf -- '- LESSON06_MODE: `%s`\n' "${LESSON06_MODE-<unset>}"
    printf -- '- LESSON06_MESSAGE: `%s`\n' "${LESSON06_MESSAGE-<unset>}"
    printf '\n## Comenzi\n\n```text\n'
    doctor || true
    printf '```\n\n## Disponibilitate contextuală\n\n'
    printf -- '- systemd user manager: `%s`\n' "$systemd_state"
    printf -- '- SSH client: `%s`\n' "$ssh_state"
    printf '\n## Contract\n\n'
    printf 'Acest raport descrie contextul observat. Manifestul versionat descrie contextul cerut. Diferența dintre ele trebuie investigată, nu ascunsă.\n\n'
    printf '> Captura de stare și acest raport sunt locale și sunt ignorate de Git. Nu enumeră întregul mediu al procesului; căile locale pot conține numele contului.\n'
  } > "$REPORT"
  printf 'snapshot=%s\nreport=%s\n' "$SNAPSHOT" "$REPORT"
}

dot_graph() {
  local output="${1:-$PROJECT_ROOT/diagrams/lesson06_workspace_flow.dot}"
  mkdir -p "$(dirname "$output")"
  cat > "$output" <<'DOT'
digraph lesson06_workspace {
  rankdir=LR;
  graph [bgcolor="white", pad="0.2", nodesep="0.35", ranksep="0.55"];
  node [fontname="DejaVu Sans", fontsize=10, shape=box, style="rounded"];
  edge [fontname="DejaVu Sans", fontsize=9];

  human [label="Utilizator / agent"];
  shell [label="Bash\ninterpretează comanda"];
  context [label="Context explicit\nCWD + PATH + mediu"];
  process [label="Proces Linux\nUID/GID + descriptori"];
  stdin [label="stdin (0)"];
  stdout [label="stdout (1)"];
  stderr [label="stderr (2)"];
  service [label="serviciu systemd\nal utilizatorului"];
  journal [label="journald / journalctl", shape=cylinder];
  remote [label="SSH / sistem\nla distanță"];
  decision [label="cod de ieșire\n0 / nenul"];

  human -> shell;
  context -> shell;
  shell -> process;
  stdin -> process;
  process -> stdout;
  process -> stderr;
  process -> decision;
  shell -> service [style=dashed, label="dacă rulează ca serviciu"];
  service -> journal;
  shell -> remote [style=dashed, label="dacă rulează la distanță"];
}
DOT
  printf '%s\n' "$output"
}

usage() {
  cat <<'EOF'
Utilizare:
  scripts/lesson06_workspace.sh doctor
  scripts/lesson06_workspace.sh context
  scripts/lesson06_workspace.sh env-demo
  scripts/lesson06_workspace.sh path-demo
  scripts/lesson06_workspace.sh pipeline-demo
  scripts/lesson06_workspace.sh systemd-preflight
  scripts/lesson06_workspace.sh ssh-preflight
  scripts/lesson06_workspace.sh report
  scripts/lesson06_workspace.sh dot [cale-ieșire]
EOF
}

main() {
  case "${1-}" in
    doctor) doctor ;;
    context) context ;;
    env-demo) env_demo ;;
    path-demo) path_demo ;;
    pipeline-demo) pipeline_demo ;;
    systemd-preflight) systemd_preflight ;;
    ssh-preflight) ssh_preflight ;;
    report) report ;;
    dot) dot_graph "${2-}" ;;
    -h|--help|help|'') usage ;;
    *) err "Subcomandă necunoscută: $1"; usage >&2; return 2 ;;
  esac
}

main "$@"
WORKSPACE_SH

Fă-l executabil și verifică sintaxa:

chmod +x scripts/lesson06_workspace.sh
bash -n scripts/lesson06_workspace.sh

Cum găsește scriptul proiectul: BASH_SOURCE[0] indică fișierul scriptului, dirname îi găsește folderul, iar pwd -P produce calea absolută fizică. Apoi scriptul urcă un nivel și obține rădăcina proiectului. De aceea funcționează chiar dacă îl pornești din /tmp.

Un detaliu important: scriptul își calculează rădăcina proiectului pornind de la locația propriului fișier. De aceea poate fi lansat și din alt CWD fără să caute manifestul „în folderul în care ai nimerit”.

12. Rulează doctor: contractul versus calculatorul real

doctor verifică două lucruri diferite: comenzile marcate required și variabilele de mediu marcate required. Pentru că ai încărcat lesson06.env.example în secțiunea 10, LESSON06_MODE trebuie să fie prezentă.

./scripts/lesson06_workspace.sh doctor
echo "$?"

Nu trebuie să obții aceleași căi ca în capturile altcuiva. Urmărește două lucruri: comenzile required trebuie să fie prezente, iar LESSON06_MODE trebuie să apară ca prezentă. Dacă lipsește una dintre aceste cerințe, doctor întoarce un cod nenul.

Rezultatul trebuie să fie 0 dacă toate comenzile required există. O componentă contextuală absentă nu trebuie să transforme spațiul de lucru de bază într-un eșec.

Testează independența față de CWD:

cd /tmp
~/ai-agents-zero-to-expert/scripts/lesson06_workspace.sh context
cd ~/ai-agents-zero-to-expert

Linia project_root trebuie să indice în continuare proiectul, chiar dacă cwd este /tmp.

13. Inspectează un context limitat, nu întregul mediu al procesului

./scripts/lesson06_workspace.sh context

Instrumentul afișează CWD, UID/GID, versiunea Bash, PATH și numai cele două variabile declarate de Lecția 6. Nu rulează env și nu enumeră întregul mediu al procesului. Reține însă că CWD și intrările PATH pot conține căi locale și, uneori, numele contului; de aceea rapoartele generate rămân locale.

Poți verifica protecția:

LESSON06_FAKE_SECRET='nu-trebuie-afisat' \
  ./scripts/lesson06_workspace.sh context \
  | grep 'nu-trebuie-afisat' \
  || echo 'secretul fictiv nu a fost afișat'

14. Demonstrează diferența dintre variabilă shell și variabilă exportată

./scripts/lesson06_workspace.sh env-demo

Trebuie să vezi:

parent_LOCAL_ONLY=doar-shell
parent_LESSON06_EXPORTED=ajunge-la-copil
child_LOCAL_ONLY=<unset>
child_LESSON06_EXPORTED=ajunge-la-copil

Aceasta este dovada practică a ideii din teorie: o variabilă obișnuită a shell-ului nu este transmisă automat procesului-copil; o variabilă exportată este inclusă în mediul lui.

15. Demonstrează că ordinea din PATH schimbă executabilul ales

./scripts/lesson06_workspace.sh path-demo
first_path=…/data/lesson06/runtime/path-a/lesson06-hello
source=path-a
second_path=…/data/lesson06/runtime/path-b/lesson06-hello
source=path-b

Partea dinainte de /data/lesson06/… depinde de calculatorul tău. Ordinea celor două rezultate este partea importantă.

Scriptul creează două executabile didactice cu același nume, în două foldere locale ignorate de Git. În prima ordine, path-a câștigă; după inversare, path-b câștigă.

Acesta este motivul pentru care command -v python este mai informativ decât presupunerea „am Python instalat”. Într-un mediu virtual, primul executabil găsit poate fi intenționat diferit.

16. Demonstrează stdout, stderr și capcana pipefail

./scripts/lesson06_workspace.sh pipeline-demo

Funcția producătoare didactică scrie date valide pe stdout, un diagnostic pe stderr și apoi iese cu codul 7. cat, ultima comandă, reușește.

Fără pipefail:

status_without_pipefail=0

Cu pipefail:

status_with_pipefail=7

În plus, rezultatul normal și diagnosticul sunt în fișiere separate. Aceasta demonstrează trei lucruri simultan: | transmite stdout, stderr rămâne separat, iar succesul ultimei comenzi nu dovedește automat succesul întregului lanț.

17. Generează captura de stare și raportul spațiului de lucru

./scripts/lesson06_workspace.sh report

Se generează:

data/lesson06/workspace_snapshot.json
docs/lessons/lesson-06/workspace_report.md

Verifică structura:

jq . data/lesson06/workspace_snapshot.json
sed -n '1,160p' docs/lessons/lesson-06/workspace_report.md

Creează un proces Bash de test. Un „heartbeat” este aici doar un mesaj periodic de stare: dovedește că procesul este încă activ și ne oferă ceva simplu de urmărit în terminal și în jurnal.

git check-ignore -v \
  data/lesson06/workspace_snapshot.json \
  docs/lessons/lesson-06/workspace_report.md

Captura de stare este intenționat locală. Nu enumeră toate variabilele de mediu, dar conține căi precum rădăcina proiectului și CWD; acestea pot dezvălui numele contului sau alte detalii locale. Faptul că fișierul este ignorat de Git este o măsură de igienă, nu o „anonimizare”.

18. Creează procesul de test pe care îl vei porni manual și ca serviciu

cat > scripts/lesson06_worker.sh <<'WORKER_SH'
#!/usr/bin/env bash
set -u

INTERVAL="${LESSON06_INTERVAL:-5}"
MAX_HEARTBEATS="${LESSON06_MAX_HEARTBEATS:-0}"
MODE="${LESSON06_MODE:-<unset>}"
count=0

[[ "$INTERVAL" =~ ^[0-9]+([.][0-9]+)?$ ]] || { printf 'EROARE: LESSON06_INTERVAL trebuie să fie un număr nenegativ.\n' >&2; exit 2; }
[[ "$MAX_HEARTBEATS" =~ ^[0-9]+$ ]] || { printf 'EROARE: LESSON06_MAX_HEARTBEATS trebuie să fie un număr întreg nenegativ.\n' >&2; exit 2; }

on_stop() {
  printf 'event=stop pid=%s mode=%s\n' "$$" "$MODE"
  exit 0
}
trap on_stop TERM INT

printf 'event=start pid=%s ppid=%s cwd=%s mode=%s\n' "$$" "$PPID" "$PWD" "$MODE"
while :; do
  count=$((count + 1))
  printf 'event=heartbeat sequence=%s pid=%s mode=%s\n' "$count" "$$" "$MODE"
  if [[ "$MAX_HEARTBEATS" =~ ^[0-9]+$ ]] && (( MAX_HEARTBEATS > 0 && count >= MAX_HEARTBEATS )); then
    printf 'event=complete heartbeats=%s\n' "$count"
    exit 0
  fi
  sleep "$INTERVAL"
done
WORKER_SH
chmod +x scripts/lesson06_worker.sh
bash -n scripts/lesson06_worker.sh

Rulează întâi o versiune finită:

LESSON06_MODE=manual LESSON06_INTERVAL=1 LESSON06_MAX_HEARTBEATS=3 ./scripts/lesson06_worker.sh

Procesul de test afișează pornirea, trei mesaje periodice de stare și finalizarea. Numerele PID/PPID și calea CWD vor fi cele ale sistemului tău.

event=start pid=… ppid=… cwd=… mode=manual
event=heartbeat sequence=1 pid=… mode=manual
event=heartbeat sequence=2 pid=… mode=manual
event=heartbeat sequence=3 pid=… mode=manual
event=complete heartbeats=3

19. Verifică dacă sesiunea poate rula un serviciu systemd al utilizatorului

./scripts/lesson06_workspace.sh systemd-preflight
printf 'exit=%s
' "$?"

Ai două rezultate corecte:

  • systemd_user_manager=available + cod 0 — continuă secțiunea systemd;
  • systemd_user_manager=unavailable + cod 3 — mediul curent nu are manager systemd al utilizatorului / magistrală de sesiune disponibil.
Nu „repara” rezultatul 3 cu sudo systemctl. Ai schimba problema: ai trece de la serviciul utilizatorului la managerul de sistem și la altă limită de privilegii. Pe containere, CI sau anumite configurații WSL, absența managerului systemd al utilizatorului este o proprietate a mediului, nu o eroare a laboratorului.

20. Creează șablonul serviciului systemd

cat > configs/systemd/lesson06-workspace.service.in <<'UNIT'
[Unit]
Description=Curs Agenți AI — Lecția 6 — demonstrație de spațiu de lucru reproductibil

[Service]
Type=simple
WorkingDirectory=__PROJECT_ROOT__
ExecStart=/usr/bin/bash __PROJECT_ROOT__/scripts/lesson06_worker.sh
Environment=LESSON06_MODE=systemd-user
Environment=LESSON06_INTERVAL=5
StandardOutput=journal
StandardError=journal
Restart=no

[Install]
WantedBy=default.target
UNIT
DirectivăCe fixează explicit
Type=simpleprocesul pornit de ExecStart este procesul principal al serviciului
WorkingDirectoryfolderul curent de lucru al procesului
ExecStartexecutabilul și scriptul care sunt pornite
Environmentvariabilele de mediu primite explicit de serviciu
StandardOutput/StandardErrordestinația stdout și stderr; aici, jurnalul systemd
WantedBy=default.targetlocul în care enable leagă unitatea pentru pornirea viitoare a managerului systemd al utilizatorului

Șablonul declară explicit trei lucruri pe care shell-ul interactiv le ascundea adesea: WorkingDirectory, executabilul din ExecStart și mediul minim.

În traseul standard al cursului, rădăcina proiectului nu conține spații. Pentru un proiect păstrat într-o cale cu spații sau caractere speciale, nu copia mecanic acest șablon: adaptează și verifică unit-ul cu instrumentele systemd.

21. Instalează copia locală a serviciului systemd al utilizatorului

Execută această secțiune numai dacă preflight-ul a raportat available.

PROJECT_ROOT="$(pwd -P)"
TEMPLATE="$(cat configs/systemd/lesson06-workspace.service.in)"
GENERATED="${TEMPLATE//__PROJECT_ROOT__/$PROJECT_ROOT}"

mkdir -p ~/.config/systemd/user
printf '%s\n' "$GENERATED" \
  > ~/.config/systemd/user/lesson06-workspace.service

systemd-analyze --user verify ~/.config/systemd/user/lesson06-workspace.service
systemctl --user daemon-reload
systemctl --user --no-pager cat lesson06-workspace.service

systemd-analyze --user verify verifică sintaxa și unele relații ale unității înainte să o pornești. Dacă raportează o eroare, oprește-te și corectează fișierul înainte de start.

Fișierul din proiect este sursa versionată. Copia din ~/.config/systemd/user/ este instalarea locală.

22. Demonstrează practic că start și enable sunt axe diferite

Pornește serviciul acum, fără să îl activezi pentru următoarea pornire relevantă a managerului systemd al utilizatorului:

systemctl --user start lesson06-workspace.service
systemctl --user is-active lesson06-workspace.service
systemctl --user is-enabled lesson06-workspace.service || true

Ar trebui să fie active, iar starea enable poate fi disabled. Acum:

systemctl --user enable lesson06-workspace.service
systemctl --user is-enabled lesson06-workspace.service

Apoi oprește-l:

systemctl --user stop lesson06-workspace.service
systemctl --user is-active lesson06-workspace.service || true
systemctl --user is-enabled lesson06-workspace.service

Acum poate fi inactive și totuși enabled. Nu este contradicție. La finalul demonstrației pornește-l din nou pentru loguri:

systemctl --user start lesson06-workspace.service

23. Urmărește stdout și stderr prin jurnal

journalctl --user   -u lesson06-workspace.service   -n 30   --no-pager

Pentru urmărire live:

journalctl --user -u lesson06-workspace.service -f

Oprește urmărirea cu Ctrl+C; serviciul rămâne pornit. Apoi:

systemctl --user --no-pager status lesson06-workspace.service

Încheie demonstrația și revino la o stare curată:

systemctl --user stop lesson06-workspace.service
systemctl --user disable lesson06-workspace.service
rm ~/.config/systemd/user/lesson06-workspace.service
systemctl --user daemon-reload

Logurile pot rămâne în journal conform politicii sistemului; copia instalată a unit-ului este eliminată.

24. Inspectează clientul SSH fără să deschizi o conexiune

Verificarea preliminară poate întoarce codul 0 dacă clientul și configurația sunt utilizabile, 3 dacă programul ssh lipsește și 4 dacă programul există, dar configurația locală nu poate fi evaluată. Niciunul dintre aceste cazuri nu deschide o conexiune SSH.

./scripts/lesson06_workspace.sh ssh-preflight
printf 'exit=%s
' "$?"

Dacă openssh-client este instalat, scriptul folosește ssh -G localhost pentru a afișa configurația rezolvată a clientului. Nu se conectează la localhost.

Inspectează și known_hosts fără să îl modifici:

ssh-keygen -F localhost || true
ls -l ~/.ssh/known_hosts 2>/dev/null || true

O conexiune la un sistem real la distanță este extensie. Nu este nevoie să instalezi un server SSH local doar pentru a „bifa” această secțiune.

25. Generează diagrama contextului de execuție

Sursa versionată este:

cat > diagrams/lesson06_workspace_flow.dot <<'DOT'
digraph lesson06_workspace {
  rankdir=LR;
  graph [bgcolor="white", pad="0.2", nodesep="0.35", ranksep="0.55"];
  node [fontname="DejaVu Sans", fontsize=10, shape=box, style="rounded"];
  edge [fontname="DejaVu Sans", fontsize=9];

  human [label="Utilizator / agent"];
  shell [label="Bash\ninterpretează comanda"];
  context [label="Context explicit\nCWD + PATH + mediu"];
  process [label="Proces Linux\nUID/GID + descriptori"];
  stdin [label="stdin (0)"];
  stdout [label="stdout (1)"];
  stderr [label="stderr (2)"];
  service [label="serviciu systemd\nal utilizatorului"];
  journal [label="journald / journalctl", shape=cylinder];
  remote [label="SSH / sistem\nla distanță"];
  decision [label="cod de ieșire\n0 / nenul"];

  human -> shell;
  context -> shell;
  shell -> process;
  stdin -> process;
  process -> stdout;
  process -> stderr;
  process -> decision;
  shell -> service [style=dashed, label="dacă rulează ca serviciu"];
  service -> journal;
  shell -> remote [style=dashed, label="dacă rulează la distanță"];
}
DOT

Poți recrea DOT-ul prin script și apoi PNG-ul:

./scripts/lesson06_workspace.sh dot \
  diagrams/lesson06_workspace_flow.dot

dot -Tpng \
  diagrams/lesson06_workspace_flow.dot \
  -o diagrams/lesson06_workspace_flow.png

file diagrams/lesson06_workspace_flow.png

26. Adaugă testele automate ale spațiului de lucru

Testele sunt scrise tot în Bash. Le creăm ca fișier complet, apoi le executăm. Astfel evaluăm exact limbajul și mecanismele lecției, fără să introducem înainte de vreme un framework Python.

cat > tests/test_lesson06_workspace.sh <<'TEST_SH'
#!/usr/bin/env bash
set -u

ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
TOOL="$ROOT/scripts/lesson06_workspace.sh"
WORKER="$ROOT/scripts/lesson06_worker.sh"
MANIFEST="$ROOT/configs/lesson06_workspace_manifest.json"
SERVICE="$ROOT/configs/systemd/lesson06-workspace.service.in"
TMP="$ROOT/data/lesson06/runtime/test-tmp"
rm -rf "$TMP"
mkdir -p "$TMP"
trap 'rm -rf "$TMP"' EXIT
pass=0
fail=0

ok() { pass=$((pass+1)); printf 'ok %02d - %s\n' "$pass" "$1"; }
not_ok() { fail=$((fail+1)); printf 'not ok - %s\n' "$1" >&2; }
assert_contains() { local hay="$1" needle="$2" name="$3"; [[ "$hay" == *"$needle"* ]] && ok "$name" || not_ok "$name (lipsește: $needle)"; }

if bash -n "$TOOL" "$WORKER"; then ok 'scripturile trec bash -n'; else not_ok 'scripturile trec bash -n'; fi
if jq -e '.schema_version==1 and .lesson==6' "$MANIFEST" >/dev/null; then ok 'manifestul JSON este valid'; else not_ok 'manifestul JSON este valid'; fi
if LESSON06_MODE=test "$TOOL" doctor >$TMP/doctor.out 2>$TMP/doctor.err; then ok 'doctor trece cu comenzile obligatorii'; else not_ok 'doctor trece cu comenzile obligatorii'; fi
if env -u LESSON06_MODE "$TOOL" doctor >$TMP/doctor-missing-env.out 2>$TMP/doctor-missing-env.err; then
  not_ok 'doctor refuză lipsa unei variabile de mediu obligatorii'
else
  ok 'doctor refuză lipsa unei variabile de mediu obligatorii'
fi

if git -C "$ROOT" check-ignore -q logs/lesson06/.gitkeep; then
  not_ok '.gitkeep din logs/lesson06 rămâne versionabil'
else
  ok '.gitkeep din logs/lesson06 rămâne versionabil'
fi


env_out="$($TOOL env-demo)"
assert_contains "$env_out" 'child_LOCAL_ONLY=<unset>' 'variabila neexportată nu ajunge la copil'
assert_contains "$env_out" 'child_LESSON06_EXPORTED=ajunge-la-copil' 'variabila exportată ajunge la copil'

path_out="$($TOOL path-demo)"
assert_contains "$path_out" 'source=path-a' 'PATH alege primul executabil din prima ordine'
assert_contains "$path_out" 'source=path-b' 'schimbarea ordinii PATH schimbă executabilul ales'

pipe_out="$($TOOL pipeline-demo)"
assert_contains "$pipe_out" 'status_without_pipefail=0' 'fără pipefail statusul vine de la ultima comandă'
assert_contains "$pipe_out" 'status_with_pipefail=7' 'cu pipefail eșecul funcției producătoare devine vizibil'
assert_contains "$pipe_out" 'diagnostic=producer-failed-after-output' 'stderr rămâne separat de stdout'

secret_out="$(LESSON06_FAKE_SECRET='nu-trebuie-afisat' "$TOOL" context)"
if [[ "$secret_out" != *'nu-trebuie-afisat'* ]]; then ok 'context nu afișează variabile arbitrare/secrete'; else not_ok 'context nu afișează variabile arbitrare/secrete'; fi

from_tmp="$(cd /tmp && "$TOOL" context)"
assert_contains "$from_tmp" "project_root=$ROOT" 'scriptul își găsește rădăcina proiectului independent de CWD'

worker_out="$(LESSON06_MODE=test LESSON06_INTERVAL=0 LESSON06_MAX_HEARTBEATS=2 "$WORKER")"
assert_contains "$worker_out" 'event=heartbeat sequence=2' 'procesul de test produce un mesaj periodic de stare'
assert_contains "$worker_out" 'event=complete heartbeats=2' 'procesul de test se poate încheia determinist'
if LESSON06_INTERVAL=invalid LESSON06_MAX_HEARTBEATS=1 "$WORKER" >$TMP/worker-invalid.out 2>$TMP/worker-invalid.err; then
  not_ok 'procesul de test refuză un interval invalid'
else
  ok 'procesul de test refuză un interval invalid'
fi

if grep -q '^WorkingDirectory=__PROJECT_ROOT__$' "$SERVICE" && grep -q '^StandardOutput=journal$' "$SERVICE" && ! grep -qi 'sudo' "$SERVICE"; then
  ok 'șablonul systemd declară CWD, jurnalul și nu folosește sudo'
else
  not_ok 'șablonul systemd declară CWD, jurnalul și nu folosește sudo'
fi

if "$TOOL" report >$TMP/report.out; then ok 'raportul local se generează'; else not_ok 'raportul local se generează'; fi
if jq -e '.schema_version==1 and (.selected_environment | has("LESSON06_MODE"))' "$ROOT/data/lesson06/workspace_snapshot.json" >/dev/null; then ok 'captura de stare este JSON structurat și limitat'; else not_ok 'captura de stare este JSON structurat și limitat'; fi
if grep -q 'Context declarat' "$ROOT/docs/lessons/lesson-06/workspace_report.md"; then ok 'raportul Markdown conține contextul declarat'; else not_ok 'raportul Markdown conține contextul declarat'; fi

DOT_TMP="$ROOT/data/lesson06/runtime/test.dot"
"$TOOL" dot "$DOT_TMP" >/dev/null
if grep -q 'CWD + PATH + mediu' "$DOT_TMP"; then ok 'diagrama păstrează contextul explicit'; else not_ok 'diagrama păstrează contextul explicit'; fi
if dot -Tpng "$DOT_TMP" -o "$ROOT/data/lesson06/runtime/test.png" && [[ -s "$ROOT/data/lesson06/runtime/test.png" ]]; then ok 'Graphviz generează diagrama'; else not_ok 'Graphviz generează diagrama'; fi

printf '\nRezultat: %d trecute, %d eșuate\n' "$pass" "$fail"
(( fail == 0 ))
TEST_SH

Testele își păstrează fișierele temporare sub data/lesson06/runtime/test-tmp/, nu în nume fixe din /tmp. Astfel două rulări nu se lovesc de fișiere rămase de la alt utilizator sau de la o execuție anterioară.

chmod +x tests/test_lesson06_workspace.sh
bash tests/test_lesson06_workspace.sh

Rezultatul verificat pentru această versiune:

23 trecute, 0 eșuate

Testele acoperă sintaxa Bash, validarea manifestului, exportul variabilelor, ordinea PATH, separarea stdout/stderr, pipefail, protecția față de variabile arbitrare, independența față de CWD, procesul de test finit, șablonul systemd, generarea raportului și generarea Graphviz.

27. Păstrează contractul spațiului de lucru ca documentație versionată

cat > docs/lessons/lesson-06/workspace-contract.md <<'MARKDOWN'
# Contractul spațiului de lucru — Lecția 6

## Scop

Acest document descrie contextul minim în care proiectul cumulativ trebuie să poată fi inspectat și operat din linia de comandă.

## Rădăcina proiectului

- Spațiu de lucru: `~/ai-agents-zero-to-expert` în traseul standard al cursului.
- Scripturile trebuie să își poată determina rădăcina proiectului pornind de la propria locație; nu trebuie să depindă exclusiv de CWD.

## Shell

- Bash 5+ pentru exemplele de shell ale cursului.
- Sintaxa specifică Bash trebuie etichetată ca atare.

## Comenzi obligatorii

Sursa canonică este `configs/lesson06_workspace_manifest.json`. `doctor` trebuie să eșueze numai dacă lipsește o comandă marcată `required`, nu pentru componente contextuale precum un manager systemd al utilizatorului sau clientul SSH.

## Variabile de mediu

- `LESSON06_MODE` — obligatorie pentru scenariile laboratorului, nu este secret.
- `LESSON06_MESSAGE` — opțională, nu este secret.
- Nu se documentează valori reale pentru tokenuri, parole sau chei API.

## I/O și coduri de ieșire

- stdout = rezultat normal;
- stderr = diagnostic;
- 0 = succes conform convenției comenzilor laboratorului;
- nenul = eroare, funcție indisponibilă sau stare care trebuie tratată explicit.

## Serviciu systemd de utilizator

Șablonul versionat este `configs/systemd/lesson06-workspace.service.in`. Copia instalată în `~/.config/systemd/user/` este stare locală, nu sursa canonică. Serviciul rulează ca utilizatorul curent, fără `sudo`.

## SSH

Laboratorul verifică existența și configurația clientului fără să instaleze sau să reconfigureze automat un server SSH. Conectarea la un sistem real la distanță este extensie, nu condiție de finalizare.

## Date locale

`workspace_snapshot.json`, `workspace_report.md`, `data/lesson06/runtime/` și logurile de execuție sunt locale și trebuie ignorate de Git.
MARKDOWN

Acesta este echivalentul operațional al fișelor de proiectare din laboratoarele anterioare. Când un viitor tool „merge doar pe calculatorul meu”, contractul îți spune ce context trebuie comparat.

28. Înregistrează decizia — ADR-0006

cat > docs/architecture-decisions/ADR-0006-explicit-command-line-context.md <<'MARKDOWN'
# ADR-0006 — Contextul liniei de comandă trebuie declarat explicit

## Stare

Acceptat pentru proiectul de învățare.

## Context

Laboratoarele anterioare au separat starea agentului, responsabilitățile stack-ului, numele produselor și autoritatea procesului. Același cod poate însă avea rezultate diferite dacă este pornit din alt folder, cu alt PATH, alt mediu, alt utilizator sau prin alt manager de procese.

## Decizie

Pentru artefactele proiectului cumulativ:

- rădăcina proiectului și CWD-ul relevant trebuie să fie explicite;
- executabilele importante trebuie rezolvate sau declarate explicit;
- variabilele de mediu necesare trebuie documentate prin nume, fără publicarea secretelor;
- stdout și stderr trebuie tratate ca fluxuri distincte;
- codurile de ieșire trebuie folosite pentru controlul determinist al pașilor;
- un serviciu systemd trebuie să primească explicit folderul de lucru, executabilul și mediul de care depinde;
- rezultatul specific calculatorului sau sesiunii nu devine automat artefact versionat;
- starea interactivă a shell-ului nu este considerată configurație reproductibilă.

## Consecințe

- Depanarea pornește de la diferențele de context, nu de la reinstalări sau `sudo`.
- Scripturile pot fi rulate din mai multe CWD-uri fără să piardă rădăcina proiectului.
- Serviciile nu depind accidental de activarea manuală a unui mediu virtual.
- Lecția 7 poate introduce Python fără să ascundă problema „ce Python rulează de fapt?”.
MARKDOWN

ADR-0006 continuă direct ADR-0005: după ce ai decis cu ce autoritate rulează procesul, decizi și în ce context de execuție poate fi considerat reproductibil.

29. Verifică proiectul înainte de commit

./scripts/lesson06_workspace.sh doctor
bash tests/test_lesson06_workspace.sh

git status --short

Confirmă rezultatele locale:

git check-ignore -v \
  data/lesson06/runtime \
  data/lesson06/workspace_snapshot.json \
  docs/lessons/lesson-06/workspace_report.md

Confirmă că sursele nu sunt ignorate:

git check-ignore \
  configs/lesson06_workspace_manifest.json \
  scripts/lesson06_workspace.sh \
  tests/test_lesson06_workspace.sh \
  && echo 'ATENȚIE: o sursă a fost ignorată' \
  || echo 'sursele rămân versionabile'

30. Salvează numai artefactele revizuite

git add \
  .gitignore \
  configs/lesson06_workspace_manifest.json \
  configs/lesson06.env.example \
  configs/systemd/lesson06-workspace.service.in \
  scripts/lesson06_workspace.sh \
  scripts/lesson06_worker.sh \
  tests/test_lesson06_workspace.sh \
  diagrams/lesson06_workspace_flow.dot \
  diagrams/lesson06_workspace_flow.png \
  docs/lessons/lesson-06/workspace-contract.md \
  docs/architecture-decisions/ADR-0006-explicit-command-line-context.md \
  logs/lesson06/.gitkeep

git diff --cached --stat
git diff --cached

git commit -m "lesson 6: make command-line context reproducible"

git diff --cached poate deschide un cititor pe mai multe ecrane. Derulează cu săgețile și apasă q pentru a ieși. Commit-ul se face numai după ce ai verificat diferențele.

31. Ce ai construit de fapt

Ai construit

  • un manifest de spațiu de lucru;
  • o configurație de mediu fără secrete;
  • un diagnostic doctor;
  • un raport limitat al contextului;
  • demonstrații reproducibile pentru export și PATH;
  • o demonstrație stdout/stderr + pipefail;
  • un proces Bash de test inspectabil;
  • un șablon de serviciu systemd al utilizatorului;
  • o probă sigură pentru clientul SSH;
  • 23 de teste Bash;
  • o diagramă Graphviz;
  • un contract de spațiu de lucru;
  • ADR-0006.

Nu ai construit încă

  • un tool Python tipizat;
  • un API;
  • o bază de date;
  • un container;
  • un pipeline CI/CD;
  • un serviciu de producție;
  • un server SSH;
  • un agent cu acces generic la shell.

32. Depanare: pornește de la context, nu de la reinstalare

doctor spune că lipsește jq sau dot

Ce vezi: linia doctor-ului cu jq | required | absent | - sau dot | required | absent | - în tabelul de comenzi.

Ce înseamnă: o dependență required — obligatorie pentru spațiul de lucru (secțiunea 9 a laboratorului) — nu a fost găsită pe mașină; fără ea, comenzile care o folosesc eșuează.

Cum confirmi: command -v jq nu afișează nimic — comanda lipsește din PATH.

Cum repari: sudo apt install jq graphviz — pe Ubuntu/Kubuntu ambele vin din APT (secțiunea 11).

Cum verifici reparația: command -v jq afișează calea (/usr/bin/jq), iar doctor reluat arată prezent pe rândul respectiv. Ieșire reală a doctor-ului pe Ubuntu 24.04, după instalare:

bash           required    prezent    /usr/bin/bash
jq             required    prezent    /usr/bin/jq
dot            required    prezent    /usr/bin/dot

Capcana: să marchezi dependența ca contextual ca să „treacă” — clasele din manifest au sensuri diferite (secțiunea 9 a laboratorului), iar o dependență obligatorie ascunsă drept contextuală minte despre spațiul de lucru.

Scriptul merge din proiect, dar nu din /tmp

Ce vezi: din proiect totul funcționează; din /tmp, același script produce erori de căi.

Ce înseamnă: scriptul calculează rădăcina proiectului din BASH_SOURCE[0] — calea fișierului script —, nu din $PWD; CWD-ul tău nu contează pentru el. Dacă ai schimbat logica să folosească doar $PWD, ai reintrodus dependența ascunsă de folderul curent (secțiunea 19.1).

Cum confirmi: rulează pwd în ambele contexte — afișează foldere diferite —, iar scriptul continuă să își găsească configurația din BASH_SOURCE.

Cum repari: păstrează calculul din BASH_SOURCE[0]; dacă vrei comenzi independente de loc, folosește căi absolute sau ancora-le în cod la rădăcina calculată.

Cum verifici reparația: bash /calea/către/scripts/lesson06_workspace.sh doctor rulat din /tmp produce același tabel ca din proiect. Capcana: „merge de aici, deci e corect” — exact raționamentul din cardul „Fișierul există, deci serviciul îl va găsi” al lecției.

LESSON06_MODE este <unset>

Ce vezi: raportul arată LESSON06_MODE=<unset>, deși ai creat fișierul de exemplu.

Ce înseamnă: fișierul .env a fost creat, dar nu a fost încărcat în shell-ul curent; el nu se încarcă singur — trebuie source-at (secțiunea 10.1).

Cum confirmi: echo "${LESSON06_MODE-unset}" înainte de încărcare afișează unset; după set -a; source configs/lesson06.env.example; set +a afișează valoarea din fișier.

Cum repari: încarcă fișierul cu set -a; source configs/lesson06.env.example; set +a sau exportă explicit variabila: export LESSON06_MODE=….

Cum verifici reparația: raportul regenerat arată LESSON06_MODE=<valoarea ta>. Capcana: să rulezi bash configs/lesson06.env.example ca „activare” — un proces copil își modifică mediul propriu, iar shell-ul tău rămâne neatins (secțiunea 10.1 a lecției).

status_without_pipefail nu este 0

Ce vezi: raportul arată status_without_pipefail diferit de 0, deși pipeline-ul din secțiunea 16 raporta 0.

Ce înseamnă: set -o pipefail a rămas activ fără restaurare: cu pipefail, pipeline-ul raportează eșecul funcției producătoare (7), nu succesul cat-ului (0). Versiunea furnizată salvează și restaurează starea opțiunii exact pentru asta.

Cum confirmi: echo $SHELLOPTS conține pipefail dacă opțiunea este activă în shell-ul curent.

Cum repari: rulează set +o pipefail în sesiune sau restaurează starea salvată de script — exact ce face versiunea furnizată în jurul demo-ului.

Cum verifici reparația: echo $SHELLOPTS nu mai conține pipefail, iar raportul revine la status_without_pipefail=0. Capcana: să setezi pipefail global „ca să prinzi tot” — globalizezi o opțiune care aparține scriptului, nu sesiunii tale.

systemd_user_manager=unavailable

Ce vezi: systemd-preflight raportează systemd_user_manager=unavailable.

Ce înseamnă: managerul systemd al utilizatorului nu este disponibil în acest mediu — normal în containere, CI, anumite sesiuni WSL sau medii fără user bus. Nu este un defect al mașinii tale.

Cum confirmi: systemctl --user is-active eșuează cu o eroare despre bus, în timp ce systemctl is-active (managerul de sistem) funcționează.

Cum repari: nimic aici: continui restul laboratorului și marchezi secțiunile 19–23 ca indisponibile în acel mediu.

Cum verifici reparația: pe un mediu cu user bus disponibil, systemctl --user is-active răspunde inactive sau starea reală. Capcana: să substitui sudo systemctl — muți demonstrația la managerul de sistem și la altă identitate, schimbând exact ce vrea laboratorul să arate (secțiunea 20 a lecției).

Serviciul spune că scriptul nu există

Ce vezi: jurnalul serviciului sau systemctl --user status lesson06-workspace.service arată că ExecStart nu găsește scriptul.

Ce înseamnă: unit-ul instalat conține o cale care nu mai există — de obicei pentru că ai mutat depozitul Git după generarea copiei.

Cum confirmi: systemctl --user cat lesson06-workspace.service — afișează conținutul copiei instalate; compară WorkingDirectory și ExecStart cu locația actuală a proiectului (pwd).

Cum repari: regenerează copia din șablon cu calea actuală și rulează systemctl --user daemon-reload.

Cum verifici reparația: systemctl --user cat arată căile noi, iar systemctl --user start lesson06-workspace.service pornește serviciul fără eroare. Capcana: să editezi doar șablonul din depozit — șablonul și copia instalată sunt două fișiere diferite (secțiunea 21 a laboratorului).

Ai modificat șablonul, dar systemd vede versiunea veche

Ce vezi: ai editat șablonul din depozitul Git, dar systemctl --user cat arată încă versiunea veche.

Ce înseamnă: șablonul din depozit și copia din ~/.config/systemd/user/ sunt două fișiere diferite: sursa versionată și starea instalată local. systemd citește copia, nu șablonul.

Cum confirmi: compară systemctl --user cat lesson06-workspace.service cu fișierul șablon din depozit — diferențele arată că copia nu a fost regenerată.

Cum repari: regenerează copia din șablon (pasul 20 al laboratorului) și rulează systemctl --user daemon-reload înainte de restart.

Cum verifici reparația: systemctl --user cat arată noul conținut, iar systemctl --user restart lesson06-workspace.service pornește serviciul cu configurația nouă. Capcana: să rulezi daemon-reload fără regenerare — reîncarcă ce există, nu ce ai editat.

ssh-preflight spune ssh_client=unavailable

Ce vezi: ssh-preflight raportează ssh_client=unavailable.

Ce înseamnă: pachetul openssh-client — clientul SSH (secțiunea 15) — nu este instalat; fără el nu există comenzi ssh, scp sau ssh-keygen.

Cum confirmi: command -v ssh nu afișează nimic.

Cum repari: sudo apt install openssh-client. Nu este nevoie să instalezi serverul SSH pentru această probă — clientul și serverul sunt pachete diferite.

Cum verifici reparația: ssh-preflight raportează ssh_client=available — ieșire reală: ssh_client=available, ssh_user=ai, ssh_hostname=localhost, ssh_port=22. Capcana: să instalezi openssh-server din reflex — proba testează clientul, nu serverul.

O variabilă secretă apare în raport

Ce vezi: raportul conține o variabilă cu valoare secretă (o cheie, un token).

Ce înseamnă: o modificare a listei de câmpuri colectate a scăpat o variabilă neprevăzută în raport — sau ai scris un secret într-una dintre variabilele didactice LESSON06_MODE și LESSON06_MESSAGE, care sunt singurele colectate de versiunea furnizată.

Cum confirmi: citește secțiunea „privacy_note” din raportul generat — ea declară exact ce colectează programul: Nu enumeră hostname-ul, întregul PATH sau mediul complet.

Cum repari: scoate variabila din lista colectată, curăță raportul generat și, dacă secretul a ajuns într-un fișier urmărit de Git, tratează-l ca compromis și rotește-l.

Cum verifici reparația: raportul regenerat conține numai LESSON06_MODE și LESSON06_MESSAGE. Capcana: să ștergi doar linia din raportul existent — colectorul o va rescrie la următoarea rulare; schimbarea se face în program, nu în fișier.

33. Exerciții practice

Exerciții de bază — termină-le înainte să consideri laboratorul încheiat

Exercițiul A — Pornește din alt CWD

Rulează doctor și report dintr-un folder temporar. Explică ce parte din script face acest lucru posibil.

Reper de verificare

Răspunsul corect: rădăcina proiectului rămâne aceeași, iar CWD-ul raportat este folderul temporar din care ai pornit comanda. Rădăcina vine din BASH_SOURCE[0] — calea fișierului script —, iar CWD-ul din mediul sesiunii; cele două sunt independente (secțiunea 19.1 a lecției).

Pas cu pas: cd /tmp schimbă doar folderul curent al shell-ului; bash /calea/proiect/scripts/lesson06_workspace.sh doctor pornește scriptul cu calea absolută; scriptul calculează rădăcina din propria locație și continuă să găsească configs/ și logs/, deși pwd arată /tmp.

Capcana: să schimbi scriptul să folosească $PWD ca rădăcină — ai reintrodus exact dependența de CWD pe care laboratorul o elimină. Pentru constructorul de agenți, acest reper verifică regulile din secțiunea 19.1: contextul de execuție nu se presupune, se inspectează.

Exercițiul B — Adaugă o comandă contextuală

Adaugă curl în manifest ca contextual. doctor trebuie să o raporteze, dar lipsa ei nu trebuie să producă eșecul spațiului de lucru de bază.

Reper de verificare

Răspunsul corect: doctor marchează curl drept contextual | absent, iar codul de ieșire rămâne 0 dacă toate cerințele required sunt îndeplinite. Clasa contextual (secțiunea 8 a lecției) înseamnă „necesară doar în anumite medii”: prezența se raportează, absența nu este eșec.

Pas cu pas: adaugi intrarea curl în manifest cu clasa contextual; rulezi doctor — rândul curl | contextual | absent | - apare în tabel fără să schimbe verdictul; doar o lipsă din clasa required ar produce eșec.

Capcana: să marchezi curl ca required „ca să fie sigur” — atunci spațiul de lucru eșuează pe mașinile fără curl, chiar dacă nimic din laborator nu îl folosește obligatoriu. Pentru constructorul de agenți, distincția clasei este ceea ce face manifestul onest: cerințele reale și cele opționale se văd separat.

Exercițiul C — Inversează PATH

Explică în cuvintele tale de ce aceeași comandă textuală lesson06-hello produce două rezultate după inversarea folderelor din PATH.

Reper de verificare

Reper: shell-ul caută în ordine; același nume poate indica alt fișier după schimbarea ordinii.

Exercițiul D — Strică intenționat funcția producătoare

Schimbă temporar codul lui producer_failure din 7 în 23. Ce se schimbă fără pipefail? Ce se schimbă cu pipefail? Revino apoi la versiunea originală.

Exercițiul E — Activ acum versus activat pentru pornire

Dacă systemd al utilizatorului este disponibil, demonstrează pe propriul sistem un moment în care serviciul este inactive și enabled. Scrie cele două comenzi care dovedesc stările.

Reper de verificare

Reper: systemctl --user is-active și systemctl --user is-enabled răspund la două întrebări diferite.

Extensii — schimbă instrumentul fără să slăbești contractul

Exercițiul F — Verifică versiuni minime

Extinde manifestul cu un câmp de versiune minimă pentru jq sau Bash. Nu compara versiuni prin ordine lexicală simplă. Indiciu: pentru versiuni de pachete Debian/Ubuntu, dpkg --compare-versions știe regulile de ordonare ale versiunilor; pentru versiunea majoră Bash, comparația numerică este suficientă.

Exercițiul G — Adaugă un log separat pentru rulare manuală

Pornește procesul de test manual cu stdout și stderr în fișiere diferite sub logs/lesson06/. Confirmă că Git le ignoră.

Exercițiul H — Sistem SSH real la distanță, fără secrete în depozit Git

Dacă ai deja un server de test, adaugă un alias în ~/.ssh/config și folosește ssh -G alias pentru a inspecta configurația. Nu copia cheia privată sau known_hosts în proiect.

Exercițiul I — Compară manual și systemd

Fă procesul de test să afișeze o variabilă nouă, ne-secretă. Export-o numai în shell. Demonstrează că serviciul utilizatorului nu o primește automat, apoi declar-o explicit în unit.

34. Quiz practic

Întrebarea 1

De ce scriptul calculează PROJECT_ROOT din BASH_SOURCE[0] și nu folosește direct $PWD?

Arată răspunsul

Pentru că rădăcina proiectului se calculează din BASH_SOURCE[0] — calea fișierului script —, iar $PWD descrie doar contextul apelantului. BASH_SOURCE[0] este calea fișierului care rulează; PWD este folderul din care a fost pornită comanda. Ele coincid doar când pornești scriptul din rădăcina proiectului.

Demonstrație reală din laborator: doctor rulat din proiect afișează Rădăcină proiect: /tmp/lab6; rulat din /tmp cu cale absolută, afișează aceeași rădăcină — deși pwd arată /tmp. Cu $PWD în locul calculului, același script ar fi căutat configs/ și logs/ în /tmp.

Capcana: „merge de aici” folosit ca argument pentru $PWD. Pentru constructorul de agenți, distincția este cea din secțiunea 19.1 a lecției: contextul de execuție nu se presupune — și rădăcina calculată din sursă este ceea ce face scriptul rulabil de oriunde.

Întrebarea 2

De ce manifestul are clase required și contextual?

Arată răspunsul

Clasele required și contextual separă cerințele proiectului de bază de componentele care depind de mediu — de pildă existența unei sesiuni systemd pentru utilizator. required înseamnă „fără acest instrument laboratorul eșuează”; contextual înseamnă „necesar doar în anumite medii”: prezența se raportează, absența nu produce eșec.

Demonstrație reală din doctor, pe Ubuntu 24.04:

systemctl      contextual  prezent    /usr/bin/systemctl
tree           optional    absent     -

systemctl este contextual — dacă user bus-ul lipsește, laboratorul continuă fără secțiunile systemd; tree este optional — nici măcar nu contează.

Capcana: să marchezi totul required „ca să fie sigur” — atunci spațiul de lucru eșuează în medii unde laboratorul ar putea rula oricum. Pentru constructorul de agenți, clasele din manifest sunt contractul de dependențe: ele spun exact ce trebuie prezent, ce poate lipsi și ce nu contează.

Întrebarea 3

De ce lesson06.env.example poate fi versionat, iar un fișier cu chei reale nu?

Arată răspunsul

lesson06.env.example poate fi versionat pentru că conține numai numele variabilelor și valori didactice nesensibile; un fișier cu credențiale reale nu, pentru că istoria Git păstrează pentru totdeauna tot ce a fost comis. Fișierul .env.example este șablonul public al configurației; credentialele reale stau într-un fișier local ignorat de Git.

Exemplu concret: șablonul laboratorului conține LESSON06_MODE=… și LESSON06_MESSAGE=… cu valori didactice; dacă ai scrie acolo o cheie API reală, ea ar rămâne recuperabilă în istorie chiar după ștergere (secțiunea 32 a laboratorului, „variabila secretă apare în raport”).

Capcana: să crezi că ștergerea fișierului „curăță” istoria. Pentru constructorul de agenți, regula separă ce se publică de ce se păstrează local: șablonul cu nume în Git, valorile reale în fișierul local ignorat — iar secretul compromis se rotește, nu se șterge.

Întrebarea 4

Ce dovedește demo-ul PATH?

Arată răspunsul

Demo-ul PATH dovedește că numele unei comenzi nu identifică singur executabilul: ordinea folderelor din PATH poate schimba fișierul care este pornit. PATH-ul este lista ordonată de foldere în care shell-ul caută comanda; primul executabil găsit câștigă (secțiunea 5.1).

Demonstrație reală din laborator: două foldere cu câte un program lesson06-hello diferit, inversate în PATH:

first_path=/tmp/lab6/data/lesson06/runtime/path-a/lesson06-hello
source=path-a
second_path=/tmp/lab6/data/lesson06/runtime/path-b/lesson06-hello
source=path-b

Același nume tastat, două programe diferite — singura schimbare a fost ordinea folderelor.

De ce este periculos: dacă un folder care nu este de încredere stă în fața folderului comenzilor de sistem, un fișier ostil cu numele ls devine comanda ls a agentului. Pentru constructorul de agenți, demo-ul justifică două reguli: folderul curent nu intră niciodată în PATH-ul agentului, iar ordinea folderelor din PATH este parte din contractul spațiului de lucru.

Întrebarea 5

De ce funcția producătoare poate eșua cu 7, iar pipeline-ul fără pipefail poate raporta 0?

Arată răspunsul

Pentru că, în mod implicit, codul de ieșire al unui pipeline Bash este codul ultimei comenzi: funcția producătoare poate eșua cu 7, iar cat de la capătul pipe-ului reușește și raportează 0. Pipeline-ul este lanțul producător–consumator conectat prin pipe; fără set -o pipefail, eșecul din amonte nu se propagă.

Demonstrație reală din laborator — pipeline-demo afișează ambele stări:

status_without_pipefail=0
status_with_pipefail=7

Cu pipefail activ, pipeline-ul raportează eșecul producătorului (7) — exact ce s-a întâmplat real.

Capcana: să citești 0 ca „totul e bine” fără să știi ce compară shell-ul. Pentru constructorul de agenți, asta justifică set -o pipefail în scripturile agentului: erorile din amonte trebuie să oprească lanțul, nu să fie mascate de un consumator care reușește.

Întrebarea 6

De ce raportul nu face pur și simplu env?

Arată răspunsul

Pentru că mediul poate conține secrete și date fără legătură cu lecția — env le-ar copia pe toate în raport, iar raportul ajunge în fișiere și în istoricul Git. Mediul (environment) conține de obicei chei API, token-uri și căi personale, nu doar variabilele didactice ale laboratorului.

Exemplu concret: raportul generat de laborator afișează explicit doar LESSON06_MODE și LESSON06_MESSAGE și încheie cu nota reală de confidențialitate:

privacy_note=Ieșirea nu enumeră întregul mediu al procesului. CWD și PATH pot conține căi locale; nu publica rezultatul fără verificare.

Capcana: să adaugi env în raport „ca să fie complet”. Pentru constructorul de agenți, colectarea minimă este regula din Lecția 2: raportul spune exact ce a observat — și nu copiază mediul pentru că ar publica secretele sesiunii tale.

Întrebarea 7

Ce diferență există între șablonul systemd din depozitul Git și copia din ~/.config/systemd/user/?

Arată răspunsul

Șablonul din depozitul Git este sursa versionată; copia din ~/.config/systemd/user/ este starea instalată local, cu calea concretă a proiectului de pe acea mașină. Șablonul se schimbă în Git și se revede de oricine; copia este ce citește systemd și conține căile absolute ale acestei instalări.

Povestea, în ordine: editezi șablonul în depozit; regenerezi copia cu căile proiectului; rulezi systemctl --user daemon-reload; abia apoi systemd vede versiunea nouă. Doi pași, două fișiere — sărind regenerarea, systemd rămâne la vechi (exact cardul „Ai modificat șablonul, dar systemd vede versiunea veche”).

Capcana: să editezi direct copia instalată — funcționează până la următoarea regenerare, care o suprascrie. Pentru constructorul de agenți, regula este cea a oricărei surse generate: se schimbă sursa versionată, iar copia se produce din ea.

Întrebarea 8

De ce nu folosim sudo systemctl când systemctl --user nu funcționează?

Arată răspunsul

Pentru că sudo systemctl ar muta serviciul la managerul de sistem și la altă identitate cu alte limite de privilegii — nu ar repara lipsa managerului de utilizator, ci ar schimba arhitectura demonstrației. systemd --user este managerul per-utilizator, care rulează serviciul cu identitatea ta; managerul de sistem rulează serviciile cu identitățile lor, de obicei root sau conturi dedicate.

Exemplu concret: un serviciu de utilizator pornit prin sudo systemctl start ar rula ca root sau ca utilizatorul din unit — iar fișierele proiectului, cu permisiunile lor, ar fi accesate cu alte drepturi decât au fost proiectate (secțiunea 20 a lecției).

Capcana: să tratezi eroarea de bus ca „systemd nu merge” și să sari la sudo. Pentru constructorul de agenți, regula este: dacă managerul de utilizator nu este disponibil în mediu, marchezi secțiunea indisponibilă și continui — nu schimbi arhitectura pentru a masca o limită de mediu.

Întrebarea 9

ssh -G localhost se conectează la server?

Arată răspunsul

Nu — ssh -G localhost afișează configurația clientului rezultată pentru gazda respectivă și apoi iese, fără să deschidă nicio conexiune. ssh -G este modul de diagnostic al clientului SSH: printează efectiv toate opțiunile aplicate — hostname, utilizator, chei, opțiuni — după aplicarea fișierelor de configurare și a aliasurilor.

Demonstrație reală: ssh -G alias pe un alias definit în ~/.ssh/config arată hostname-ul real, utilizatorul, portul și fișierele de chei folosite — tot ce ar folosi o conexiune reală, fără să o inițieze.

Capcana: să crezi că „-G” conectează și apoi afișează. Pentru constructorul de agenți, aceasta este metoda sigură de a verifica ce va face o conexiune înainte să o faci — utilă exact când un alias sau un config ar putea ascunde setări neașteptate.

Întrebarea 10

Care este legătura dintre ADR-0005 și ADR-0006?

Arată răspunsul

ADR-0005 delimitează autoritatea procesului agent, iar ADR-0006 declară contextul în care acel proces este pornit și observat — identitatea corectă fără PATH, CWD și mediu corect nu produce un spațiu de lucru reproductibil. ADR (Architecture Decision Record) este documentul care consemnează o decizie de arhitectură cu contextul și consecințele ei.

Povestea legăturii, în ordine: ADR-0005 (Lecția 5) stabilește cu ce identitate și ce drepturi rulează agentul; ADR-0006 (această lecție) stabilește unde rulează, cu ce PATH, ce CWD, ce variabile de mediu și unde ajung logurile. Ambele împreună descriu contextul complet de execuție din secțiunea 16 a teoriei.

Capcana: să scrii un ADR doar despre identitate și să ignori contextul — sau invers. Pentru constructorul de agenți, cele două documente împreună răspund la „cine și cu ce drepturi” plus „unde și cu ce mediu” — setul complet de condiții care face execuția reproductibilă și depanabilă.

35. Checklist de final

  • [ ] ai continuat proiectul cumulativ, nu ai creat alt depozit Git;
  • [ ] manifestul Lecției 6 trece jq empty;
  • [ ] fișierul de mediu conține numai valori didactice;
  • [ ] doctor trece;
  • [ ] instrumentul își găsește rădăcina proiectului din alt CWD;
  • [ ] poți explica de ce o variabilă neexportată lipsește în copil;
  • [ ] demo-ul PATH arată două executabile diferite;
  • [ ] demo-ul pipeline arată 0 fără pipefail și 7 cu pipefail;
  • [ ] captura de stare nu afișează variabile arbitrare;
  • [ ] procesul de test manual poate termina determinist;
  • [ ] dacă systemd al utilizatorului este disponibil, ai demonstrat active versus enabled;
  • [ ] ai citit logurile procesului de test prin journalctl;
  • [ ] ai curățat copia locală a unit-ului după exercițiu;
  • [ ] ai rulat preflight-ul SSH fără conectare;
  • [ ] toate cele 23 de teste trec;
  • [ ] Graphviz generează PNG-ul;
  • [ ] rezultatele locale sunt ignorate de Git;
  • [ ] contractul spațiului de lucru și ADR-0006 sunt versionate;
  • [ ] ai inspectat diff-ul înainte de commit.

36. Verificarea executată pentru această versiune

Înainte de publicarea laboratorului am executat partea reproductibilă într-un mediu Linux curat:

  • Bash syntax: toate scripturile trec bash -n;
  • manifest: JSON valid;
  • teste: 23/23 trec;
  • CWD independence: verificată prin lansare din /tmp;
  • mediu: variabila neexportată nu ajunge la copil, cea exportată ajunge;
  • PATH: ordinea schimbă executabilul ales;
  • pipefail: 0 fără pipefail, 7 cu pipefail pentru aceeași funcție producătoare;
  • privacy hygiene: o variabilă secretă fictivă nu apare în context;
  • proces de test: mesaje periodice de stare și terminare deterministă verificate;
  • unitate systemd: șablonul substituit trece systemd-analyze verify;
  • Graphviz: DOT-ul este randat într-un PNG valid;
  • systemd și SSH: integrarea live a fost verificată pe un desktop Ubuntu 24.04 (manager systemd al utilizatorului disponibil, openssh-client instalat); în mediul CI fără user bus și fără client SSH, verificările preliminare raportează corect indisponibilitatea, fără oprire cu eroare.

Pe Ubuntu/Kubuntu desktop, comenzile din secțiunile 19–24 sunt furnizate pentru verificare directă de către cursant.

37. Ce păstrăm pentru Lecția 7

După Laboratorul 6, proiectul nu are doar cod; are și un contract operațional. Știi ce shell presupune, ce executabile caută, ce mediu este declarat, cum se propagă erorile prin pipeline, cum diferențiezi rezultatul de codul de ieșire și cum se schimbă contextul când același proces de test devine serviciu.

În Lecția 7 — Python, de la zero la util, aceste întrebări nu dispar. Dimpotrivă, devin fundația primului tool Python tipizat: vei ști ce python rulează, din ce mediu, cum primește argumente și variabile, ce cod de ieșire produce și cum îl poate porni ulterior un controller.